com.aelitis.azureus.core.networkmanager.impl.ProtocolDecoderPHE.java Source code

Java tutorial

Introduction

Here is the source code for com.aelitis.azureus.core.networkmanager.impl.ProtocolDecoderPHE.java

Source

/*
 * Created on 17-Jan-2006
 * Created by Paul Gardner
 * Copyright (C) 2006 Aelitis, All Rights Reserved.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 * 
 * AELITIS, SAS au capital de 46,603.30 euros
 * 8 Allee Lenotre, La Grille Royale, 78600 Le Mesnil le Roi, France.
 *
 */

package com.aelitis.azureus.core.networkmanager.impl;

import java.io.IOException;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.util.*;

import javax.crypto.Cipher;
import javax.crypto.KeyAgreement;
import javax.crypto.interfaces.DHPublicKey;
import javax.crypto.spec.DHParameterSpec;
import javax.crypto.spec.DHPublicKeySpec;
import javax.crypto.spec.SecretKeySpec;

import org.bouncycastle.util.encoders.Base64;
import org.gudy.azureus2.core3.config.COConfigurationManager;
import org.gudy.azureus2.core3.config.ParameterListener;
import org.gudy.azureus2.core3.logging.LogAlert;
import org.gudy.azureus2.core3.logging.LogEvent;
import org.gudy.azureus2.core3.logging.LogIDs;
import org.gudy.azureus2.core3.logging.Logger;
import org.gudy.azureus2.core3.util.AEMonitor;
import org.gudy.azureus2.core3.util.ByteFormatter;
import org.gudy.azureus2.core3.util.Debug;
import org.gudy.azureus2.core3.util.HashWrapper;
import org.gudy.azureus2.core3.util.SHA1Hasher;
import org.gudy.azureus2.core3.util.SystemTime;

import com.aelitis.azureus.core.networkmanager.NetworkManager;
import com.aelitis.azureus.core.networkmanager.impl.osssl.OneSwarmSslTransportHelperFilterStream;
import com.aelitis.azureus.core.networkmanager.impl.osssl.OneSwarmSslTransportHelperFilterStream.SslHandShakeMatch;
import com.aelitis.azureus.core.util.bloom.BloomFilter;
import com.aelitis.azureus.core.util.bloom.BloomFilterFactory;

public class ProtocolDecoderPHE extends ProtocolDecoder {
    private static final LogIDs LOGID = LogIDs.NWMAN;

    private static final byte CRYPTO_PLAIN = 0x01;
    private static final byte CRYPTO_RC4 = 0x02;
    private static final byte CRYPTO_XOR = 0x04;
    private static final byte CRYPTO_AES = 0x08;
    // ****************************************************
    private static final byte CRYPTO_SSL = 0x10;
    // ****************************************************

    //private static final String    DH_P = "92d862b3a95bff4e6cbdce3a266ff4b46e6e1ecad76c0a877d92a3dae4999e6414efde56fc14d1cca6d5408a8ef9ea248389168876b6e8f4503845dfe373549f";
    //private static final String    DH_G = "4383b53ee650fd73e41e8c9e8527997ab8cb41e1cbd73ac7685493e1e5d091e3e3789dea03ab9d5b2c368faa617bb30e427cbaeb23c268edb38eb8c747756080";
    // private static final String    DH_P = "f3f90c790c63b119f9c1be43fdb12dc6ed6f26325999c01ba6ed373e75d6b2dee8d1c0475652a987c8df57b23d395bdb142be316d780b9361f85629535030873";

    private static final String DH_P = "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A63A36210000000000090563";
    private static final String DH_G = "02";
    private static final int DH_L = 160;

    private static final int DH_SIZE_BYTES = DH_P.length() / 2;

    public static final int MIN_INCOMING_INITIAL_PACKET_SIZE = DH_SIZE_BYTES;

    private static final BigInteger DH_P_BI = new BigInteger(DH_P, 16);
    private static final BigInteger DH_G_BI = new BigInteger(DH_G, 16);

    private static KeyPairGenerator dh_key_generator;
    private static long last_dh_incoming_key_generate;

    private static final int BLOOM_RECREATE = 30 * 1000;
    private static final int BLOOM_INCREASE = 1000;
    private static BloomFilter generate_bloom = BloomFilterFactory.createAddRemove4Bit(BLOOM_INCREASE);
    private static long generate_bloom_create_time = SystemTime.getCurrentTime();

    private static boolean crypto_ok;
    //private static boolean   aes_ok;

    /*
    private static final String      AES_STREAM_ALG            = "AES";
    private static final String      AES_STREAM_CIPHER         = "AES/CFB8/NoPadding";
    private static final int      AES_STREAM_KEY_SIZE         = 128;
    private static final int      AES_STREAM_KEY_SIZE_BYTES   = AES_STREAM_KEY_SIZE/8;
       */

    //private static final byte[]      AES_STREAM_IV            = 
    //   {    (byte)0x15, (byte)0xE0, (byte)0x6B, (byte)0x7E, (byte)0x98, (byte)0x59, (byte)0xE4, (byte)0xA7, 
    //      (byte)0x34, (byte)0x66, (byte)0xAD, (byte)0x48, (byte)0x35, (byte)0xE2, (byte)0xD0, (byte)0x24 };

    private static final String RC4_STREAM_ALG = "RC4";
    private static final String RC4_STREAM_CIPHER = "RC4";
    private static final int RC4_STREAM_KEY_SIZE = 128;
    private static final int RC4_STREAM_KEY_SIZE_BYTES = RC4_STREAM_KEY_SIZE / 8;

    private static final int PADDING_MAX = 512;

    private static final int PADDING_MAX_NORMAL = PADDING_MAX;
    private static final int PADDING_MAX_LIMITED = 128;

    public static int getMaxIncomingInitialPacketSize(boolean min_overheads) {
        return (MIN_INCOMING_INITIAL_PACKET_SIZE + (min_overheads ? PADDING_MAX_LIMITED : PADDING_MAX_NORMAL) / 2);
    }

    private static final Random random = new SecureRandom();

    private static Map global_shared_secrets = new HashMap();

    static {
        try {
            DHParameterSpec dh_param_spec = new DHParameterSpec(DH_P_BI, DH_G_BI, DH_L);

            dh_key_generator = KeyPairGenerator.getInstance("DH");

            dh_key_generator.initialize(dh_param_spec);

            dh_key_generator.generateKeyPair();

            byte[] rc4_test_secret = new byte[RC4_STREAM_KEY_SIZE_BYTES];

            SecretKeySpec rc4_test_secret_key_spec = new SecretKeySpec(rc4_test_secret, 0,
                    RC4_STREAM_KEY_SIZE_BYTES, RC4_STREAM_ALG);

            TransportCipher rc4_cipher = new TransportCipher(RC4_STREAM_CIPHER, Cipher.ENCRYPT_MODE,
                    rc4_test_secret_key_spec);

            rc4_cipher = new TransportCipher(RC4_STREAM_CIPHER, Cipher.DECRYPT_MODE, rc4_test_secret_key_spec);

            /*
            try{
              byte[]   aes_test_secret = new byte[AES_STREAM_KEY_SIZE_BYTES];
                     
              SecretKeySpec   aes_test_secret_key_spec = new SecretKeySpec(aes_test_secret, 0, AES_STREAM_KEY_SIZE_BYTES, AES_STREAM_ALG );
                         
              AlgorithmParameterSpec   spec =    new IvParameterSpec( aes_test_secret );
                    
                TCPTransportCipher aes_cipher = new TCPTransportCipher( AES_STREAM_CIPHER, Cipher.ENCRYPT_MODE, aes_test_secret_key_spec, spec );
                    
                aes_cipher = new TCPTransportCipher( AES_STREAM_CIPHER, Cipher.DECRYPT_MODE, aes_test_secret_key_spec, spec );
                    
                aes_ok   = true;
                    
            }catch( Throwable e ){
                  
              Logger.log(   new LogEvent(LOGID, "AES Unavailable", e ));
            }
             */

            crypto_ok = true;

            if (Logger.isEnabled()) {

                Logger.log(new LogEvent(LOGID, "PHE crypto initialised"));
            }
        } catch (NoClassDefFoundError e) {

            // running without PHE classes, not such a severe error

            Logger.log(new LogEvent(LOGID, "PHE crypto disabled as classes unavailable"));

            crypto_ok = false;

        } catch (Throwable e) {

            Logger.log(new LogEvent(LOGID, "PHE crypto initialisation failed", e));

            crypto_ok = false;
        }
    }

    public static boolean isCryptoOK() {
        return (crypto_ok);
    }

    public static void addSecretsSupport(byte[][] secrets) {
        for (int i = 0; i < secrets.length; i++) {

            SHA1Hasher hasher = new SHA1Hasher();

            hasher.update(REQ2_IV);
            hasher.update(secrets[i]);

            byte[] encoded = hasher.getDigest();

            synchronized (global_shared_secrets) {

                global_shared_secrets.put(new HashWrapper(encoded), secrets[i]);
            }
        }
    }

    public static void removeSecretsSupport(byte[][] secrets) {
        for (int i = 0; i < secrets.length; i++) {

            SHA1Hasher hasher = new SHA1Hasher();

            hasher.update(REQ2_IV);
            hasher.update(secrets[i]);

            byte[] encoded = hasher.getDigest();

            synchronized (global_shared_secrets) {

                global_shared_secrets.remove(new HashWrapper(encoded));
            }
        }
    }

    // private static final byte SUPPORTED_PROTOCOLS = (byte)((aes_ok?CRYPTO_AES:0) | CRYPTO_RC4 | CRYPTO_XOR | CRYPTO_PLAIN );
    // *****************************************
    private static final byte SUPPORTED_PROTOCOLS = (byte) (CRYPTO_RC4 | CRYPTO_PLAIN | CRYPTO_SSL);
    // *****************************************

    private static byte MIN_CRYPTO;

    static {
        COConfigurationManager.addAndFireParameterListeners(
                new String[] { "network.transport.encrypted.min_level" }, new ParameterListener() {
                    public void parameterChanged(String ignore) {
                        if (NetworkManager.REQUIRE_CRYPTO_HANDSHAKE && !isCryptoOK()) {
                            Logger.log(new LogAlert(true, LogAlert.AT_ERROR,
                                    "Connection encryption unavailable, please update your Java version"));
                        }

                        String min = COConfigurationManager
                                .getStringParameter("network.transport.encrypted.min_level");

                        // ***********************************************************
                        if (min.equals("XOR")) {

                            MIN_CRYPTO = CRYPTO_XOR | CRYPTO_RC4 | CRYPTO_AES | CRYPTO_SSL;

                        } else if (min.equals("RC4")) {

                            MIN_CRYPTO = CRYPTO_RC4 | CRYPTO_AES | CRYPTO_SSL;

                        } else if (min.equals("AES")) {

                            MIN_CRYPTO = CRYPTO_AES | CRYPTO_SSL;

                        } else if (min.equals("SSL")) {

                            MIN_CRYPTO = CRYPTO_SSL;

                        } else {

                            MIN_CRYPTO = CRYPTO_PLAIN | CRYPTO_XOR | CRYPTO_RC4 | CRYPTO_AES | CRYPTO_SSL;
                        }
                        // ************************************************************

                        MIN_CRYPTO = (byte) (MIN_CRYPTO & SUPPORTED_PROTOCOLS);
                    }
                });
    }

    private static final int PS_OUTBOUND_1 = 0;
    private static final int PS_OUTBOUND_2 = 1;
    private static final int PS_OUTBOUND_3 = 2;
    private static final int PS_OUTBOUND_4 = 3;

    private static final int PS_INBOUND_1 = 10;
    private static final int PS_INBOUND_2 = 11;
    private static final int PS_INBOUND_3 = 12;
    private static final int PS_INBOUND_4 = 13;

    public static final byte[] KEYA_IV = "keyA".getBytes();
    public static final byte[] KEYB_IV = "keyB".getBytes();
    public static final byte[] REQ1_IV = "req1".getBytes();
    public static final byte[] REQ2_IV = "req2".getBytes();
    public static final byte[] REQ3_IV = "req3".getBytes();
    public static final byte[] VC = { 0, 0, 0, 0, 0, 0, 0, 0 };

    private TransportHelper transport;
    private ByteBuffer write_buffer;
    private ByteBuffer read_buffer;

    private ProtocolDecoderAdapter adapter;

    private KeyAgreement key_agreement;
    private byte[] dh_public_key_bytes;

    private byte[] shared_secret;
    private byte[] secret_bytes;

    private ByteBuffer initial_data_out;
    private ByteBuffer initial_data_in;

    private TransportCipher write_cipher;
    private TransportCipher read_cipher;

    private byte[] padding_skip_marker;

    private byte my_supported_protocols;
    private byte selected_protocol;

    private boolean outbound;

    private int protocol_state;
    private int protocol_substate;

    private boolean handshake_complete;

    private int bytes_read;
    private int bytes_written;

    private long last_read_time = SystemTime.getCurrentTime();

    private TransportHelperFilter filter;

    private boolean delay_outbound_4;

    private boolean processing_complete;

    private AEMonitor process_mon = new AEMonitor("ProtocolDecoderPHE:process");

    // ****************************************************
    private boolean outBoundSSL = false;
    private OneSwarmSslTransportHelperFilterStream sslFilter;
    private byte[] expectedPublicKey;
    private byte[][] sharedSecrets;
    private SslHandShakeMatch sslMatch;
    // ****************************************************

    public ProtocolDecoderPHE(TransportHelper _transport, byte[][] _shared_secrets, ByteBuffer _header,
            ByteBuffer _initial_data, ProtocolDecoderAdapter _adapter)

            throws IOException {
        super(false);

        if (!isCryptoOK()) {

            throw (new IOException("PHE crypto broken"));
        }

        transport = _transport;
        // scattering mode for the first KB, that should include the crypto handshake and part of the bittorrent handshake
        transport.setScatteringMode(768 + random.nextInt(256));
        initial_data_out = _initial_data;
        adapter = _adapter;

        // **********************************************************
        // check if we should use outgoing SSL (sorry for the hack)
        if (_shared_secrets != null && _shared_secrets[0] != null && new String(_shared_secrets[0])
                .equals(OneSwarmSslTransportHelperFilterStream.SHARED_SECRET_FOR_SSL_STRING)) {
            this.outBoundSSL = true;
            // store the remote public key
            expectedPublicKey = _shared_secrets[1];
            sharedSecrets = _shared_secrets;
            //         System.out.println("starting new PHE: expected remote pubkey:\n" + 
            //               new String(Base64.encode(expectedPublicKey)));
        } else if (_shared_secrets == null || _shared_secrets.length == 0) {
            // **********************************************************
            shared_secret = new byte[0];

        } else {

            if (_shared_secrets.length == 1) {

                shared_secret = _shared_secrets[0];

            } else {

                shared_secret = _shared_secrets[random.nextInt(_shared_secrets.length)];
            }

            // System.out.println( "outbound - using crypto secret " + ByteFormatter.encodeString( shared_secret ));     
        }

        outbound = _header == null;

        my_supported_protocols = SUPPORTED_PROTOCOLS;

        if (outbound) {

            //if ( !NetworkManager.REQUIRE_CRYPTO_HANDSHAKE ){            
            //   throw( new IOException( "Crypto encoder selected for outbound but crypto not required" ));
            //}

            // outbound connection, we require a certain minimal level of support

            my_supported_protocols = MIN_CRYPTO;

        } else {

            // incoming. If we require crypto then we use minimum otherwise available

            if (NetworkManager.REQUIRE_CRYPTO_HANDSHAKE) {

                my_supported_protocols = MIN_CRYPTO;
            }
        }

        /*
         * only init the crypto if isn't ssl, the dh keygeneration is ratelimited by az
         */
        if (!outbound) {
            ByteBuffer clone = _header.duplicate();
            byte[] earlyData = new byte[clone.remaining()];
            clone.get(earlyData);
            sslMatch = isSSL(earlyData);
        }
        if (sslMatch == SslHandShakeMatch.NO_SSL || sslMatch == SslHandShakeMatch.NOT_ENOUGH_BYTES
                || sslMatch == null) {
            // don't init the crypto unless it might be useful
            initCrypto();
        }

        try {
            process_mon.enter();

            transport.registerForReadSelects(new TransportHelper.selectListener() {
                public boolean selectSuccess(TransportHelper helper, Object attachment) {
                    return (ProtocolDecoderPHE.this.selectSuccess(helper, attachment, false));
                }

                public void selectFailure(TransportHelper helper, Object attachment, Throwable msg) {
                    ProtocolDecoderPHE.this.selectFailure(helper, attachment, msg);
                }
            }, null);

            transport.registerForWriteSelects(new TransportHelper.selectListener() {
                public boolean selectSuccess(TransportHelper helper, Object attachment) {
                    return (ProtocolDecoderPHE.this.selectSuccess(helper, attachment, true));
                }

                public void selectFailure(TransportHelper helper, Object attachment, Throwable msg) {
                    ProtocolDecoderPHE.this.selectFailure(helper, attachment, msg);
                }
            }, null);

            transport.pauseWriteSelects();

            if (outbound) {

                protocol_state = PS_OUTBOUND_1;

                transport.pauseReadSelects();

            } else {

                protocol_state = PS_INBOUND_1;

                if (dh_public_key_bytes != null) {
                    read_buffer = ByteBuffer.allocate(dh_public_key_bytes.length);
                } else {
                    read_buffer = ByteBuffer.allocate(96);
                }

                read_buffer.put(_header);

                bytes_read += _header.limit();
            }
        } finally {

            process_mon.exit();
        }

        process();
    }

    private static SslHandShakeMatch isSSL(byte[] data) {
        if (data.length > OneSwarmSslTransportHelperFilterStream.SSL_HEADER_MIN_LENGTH) {
            SslHandShakeMatch isClientHello = OneSwarmSslTransportHelperFilterStream.isSSLClientHello(data);
            //         System.out.println("ssl match: " + isClientHello.name());
            return isClientHello;
        }
        return SslHandShakeMatch.NOT_ENOUGH_BYTES;
    }

    protected void initCrypto()

            throws IOException {
        try {
            KeyPair key_pair = generateDHKeyPair(transport, outbound);

            key_agreement = KeyAgreement.getInstance("DH");

            key_agreement.init(key_pair.getPrivate());

            DHPublicKey dh_public_key = (DHPublicKey) key_pair.getPublic();

            BigInteger dh_y = dh_public_key.getY();

            dh_public_key_bytes = bigIntegerToBytes(dh_y, DH_SIZE_BYTES);

        } catch (Throwable e) {

            throw (new IOException(Debug.getNestedExceptionMessage(e)));
        }
    }

    protected void completeDH(byte[] buffer)

            throws IOException {
        try {
            BigInteger other_dh_y = bytesToBigInteger(buffer, 0, DH_SIZE_BYTES);

            KeyFactory dh_key_factory = KeyFactory.getInstance("DH");

            PublicKey other_public_key = dh_key_factory
                    .generatePublic(new DHPublicKeySpec(other_dh_y, DH_P_BI, DH_G_BI));

            key_agreement.doPhase(other_public_key, true);

            secret_bytes = key_agreement.generateSecret();

            adapter.gotSecret(secret_bytes);

            // System.out.println( "secret = " + ByteFormatter.encodeString( secret_bytes ));

        } catch (Throwable e) {
            e.printStackTrace();
            throw (new IOException(Debug.getNestedExceptionMessage(e)));
        }
    }

    protected void setupCrypto()

            throws IOException {
        try {
            //"HASH('keyA', S, SKEY)" if you're A
            //"HASH('keyB', S, SKEY)" if you're B

            SHA1Hasher hasher = new SHA1Hasher();

            hasher.update(KEYA_IV);
            hasher.update(secret_bytes);
            hasher.update(shared_secret);

            byte[] a_key = hasher.getDigest();

            hasher = new SHA1Hasher();

            hasher.update(KEYB_IV);
            hasher.update(secret_bytes);
            hasher.update(shared_secret);

            byte[] b_key = hasher.getDigest();

            SecretKeySpec secret_key_spec_a = new SecretKeySpec(a_key, RC4_STREAM_ALG);

            SecretKeySpec secret_key_spec_b = new SecretKeySpec(b_key, RC4_STREAM_ALG);

            write_cipher = new TransportCipher(RC4_STREAM_CIPHER, Cipher.ENCRYPT_MODE,
                    outbound ? secret_key_spec_a : secret_key_spec_b);

            read_cipher = new TransportCipher(RC4_STREAM_CIPHER, Cipher.DECRYPT_MODE,
                    outbound ? secret_key_spec_b : secret_key_spec_a);

        } catch (Throwable e) {

            e.printStackTrace();

            throw (new IOException(Debug.getNestedExceptionMessage(e)));
        }
    }

    /*
    protected void
    completeDH(
       byte[]   buffer )
        
       throws IOException
    {
       try{         
       BigInteger   other_dh_y = bytesToBigInteger( buffer, 0, DH_SIZE_BYTES );
           
       KeyFactory dh_key_factory = KeyFactory.getInstance("DH");
                  
      PublicKey other_public_key = dh_key_factory.generatePublic( new DHPublicKeySpec( other_dh_y, DH_P_BI, DH_G_BI ));
                 
      key_agreement.doPhase( other_public_key, true );
          
      byte[]   secret_bytes_64 = key_agreement.generateSecret();
        
         // we only want the first 32 bytes of the secret
          
      secret_bytes = new byte[32];
          
      System.arraycopy( secret_bytes_64, 0, secret_bytes, 0, 32 );
          
      sha1_secret_bytes   = new SHA1Simple().calculateHash( secret_bytes );
                       
      SecretKeySpec   secret_key_spec_a = new SecretKeySpec( secret_bytes, 0, RC4_STREAM_KEY_SIZE_BYTES, RC4_STREAM_ALG );
              
      SecretKeySpec   secret_key_spec_b = new SecretKeySpec( secret_bytes, 16, RC4_STREAM_KEY_SIZE_BYTES, RC4_STREAM_ALG );
                         
      write_cipher    = new TCPTransportCipher( RC4_STREAM_CIPHER, Cipher.ENCRYPT_MODE, outbound?secret_key_spec_a:secret_key_spec_b );
             
      read_cipher    = new TCPTransportCipher( RC4_STREAM_CIPHER, Cipher.DECRYPT_MODE, outbound?secret_key_spec_b:secret_key_spec_a );
          
       }catch( Throwable e ){
         
     throw( new IOException( Debug.getNestedExceptionMessage(e)));
       }
    }
    */

    protected void handshakeComplete()

            throws IOException {
        if (selected_protocol == CRYPTO_PLAIN) {

            filter = new TransportHelperFilterTransparent(transport, true);

        } else if (selected_protocol == CRYPTO_XOR) {

            filter = new TransportHelperFilterStreamXOR(transport, secret_bytes);

        } else if (selected_protocol == CRYPTO_RC4) {

            filter = new TransportHelperFilterStreamCipher(transport, read_cipher, write_cipher);
            // **********************************************
        } else if (selected_protocol == CRYPTO_SSL) {
            filter = sslFilter;
            // **********************************************

            /*
            }else if ( selected_protocol == CRYPTO_AES ){
                
            try{
                 SecretKeySpec   secret_key_spec = new SecretKeySpec( secret_bytes, 32, AES_STREAM_KEY_SIZE_BYTES, AES_STREAM_ALG );
                               
                 AlgorithmParameterSpec   spec =    new IvParameterSpec( secret_bytes, 48, AES_STREAM_KEY_SIZE_BYTES );
                     
                 write_cipher    = new TCPTransportCipher( AES_STREAM_CIPHER, Cipher.ENCRYPT_MODE, secret_key_spec, spec );
                
                 read_cipher    = new TCPTransportCipher( AES_STREAM_CIPHER, Cipher.DECRYPT_MODE, secret_key_spec, spec );
                     
               filter = new TCPTransportHelperFilterStreamCipher( 
              helper,
              read_cipher,
              write_cipher );
                   
            }catch( Throwable e ){
                   
               throw( new IOException( "AES crypto init failed: " + Debug.getNestedExceptionMessage(e)));
            }
            */

        } else {

            throw (new IOException("Invalid selected protocol '" + selected_protocol + "'"));
        }

        if (initial_data_in != null) {

            filter = new TransportHelperFilterInserter(filter, initial_data_in);
        }

        handshake_complete = true;
    }

    /*
       X_1
           
    A->B: Diffie Hellman Ya, PadA
        
       X_2
           
       B->A: Diffie Hellman Yb, PadB
           
     X_3
         
       A->B: HASH('req1', S), HASH('req2', SKEY)^HASH('req3', S), ENCRYPT(VC, crypto_provide, len(PadC), PadC, len(IA)), ENCRYPT(IA)
           
     X_4
         
       B->A: ENCRYPT(VC, crypto_select, len(padD), padD ) // , len(IB)), ENCRYPT(IB)
    */

    protected void process()

            throws IOException {
        try {
            process_mon.enter();

            if (handshake_complete) {

                Debug.out("Handshake process already completed");

                return;
            }

            boolean loop = true;

            while (loop) {

                //System.err.println( this + ":" + (outbound?"out: ":"in : ") + protocol_state + "/" + protocol_substate + ": r " + bytes_read + " - " + read_buffer + ", w " + bytes_written + " - " + write_buffer );

                // ********************************************************
                /*
                 * Major edits, this is basically where all the SSL modifications are
                 */
                if (selected_protocol == CRYPTO_SSL) {
                    if (sslFilter.isHandshakeCompleted()) {
                        // if it was an outgoing connection
                        // check that we actually connected to the
                        // hosts we intended
                        if (outBoundSSL) {
                            byte[] remoteKey = (byte[]) transport
                                    .getUserData(OneSwarmSslTransportHelperFilterStream.REMOTE_SSL_PUBLIC_KEY);
                            if (Arrays.equals(expectedPublicKey,
                                    OneSwarmSslTransportHelperFilterStream.ANY_KEY_ACCEPTED_BYTES)) {
                                // this is an auth connection, allow anyway but copy the 
                                // remote public key into the array so the auth manager can see what it is
                                sharedSecrets[1] = remoteKey;
                            } else if (!Arrays.equals(expectedPublicKey, remoteKey)) {
                                throw new IOException("remote friend publickey error, expected to connect to \n'"
                                        + new String(Base64.encode(expectedPublicKey)) + "' got\n'"
                                        + new String(Base64.encode(remoteKey)) + "'");
                            }

                        }
                        handshakeComplete();
                        processing_complete = true;
                        loop = false;
                        write_buffer = null;
                        read_buffer = null;

                    } else {

                        if (write_buffer == null) {
                            write_buffer = ByteBuffer
                                    .allocate(OneSwarmSslTransportHelperFilterStream.SSL_APP_BUFFER_SIZE);
                            write_buffer.flip();
                        }
                        read(read_buffer);
                        sslFilter.doHandshake(read_buffer, write_buffer);
                        write(write_buffer);
                        transport.resumeReadSelects();
                        transport.resumeWriteSelects();
                        return;
                    }
                } else if (outBoundSSL) {
                    // If using ssl, and outbound
                    read_buffer = ByteBuffer.allocate(OneSwarmSslTransportHelperFilterStream.SSL_NET_BUFFER_SIZE);
                    write_buffer = ByteBuffer.allocate(OneSwarmSslTransportHelperFilterStream.SSL_APP_BUFFER_SIZE);
                    sslFilter = new OneSwarmSslTransportHelperFilterStream(transport, outbound,
                            OneSwarmSslTransportHelperFilterStream.SslHandShakeMatch.SSL_CLIENT_CERT);
                    selected_protocol = CRYPTO_SSL;

                    sslFilter.doHandshake(read_buffer, write_buffer);
                    transport.resumeReadSelects();
                    transport.resumeWriteSelects();

                } else if (protocol_state == PS_OUTBOUND_1) {
                    // ************************************************************
                    if (write_buffer == null) {

                        // A sends B Ya + Pa

                        byte[] padding_a = getRandomPadding(getPaddingMax() / 2); // note that /2 also used in calculating max initial packet size above                           

                        write_buffer = ByteBuffer.allocate(dh_public_key_bytes.length + padding_a.length);

                        write_buffer.put(dh_public_key_bytes);

                        write_buffer.put(padding_a);

                        write_buffer.flip();
                    }

                    write(write_buffer);

                    if (!write_buffer.hasRemaining()) {

                        write_buffer = null;

                        protocol_state = PS_INBOUND_2;
                    }

                } else if (protocol_state == PS_INBOUND_1) {

                    // B receives Ya 

                    read(read_buffer);

                    // *******************************************************
                    // check if the incoming connection is a SSL connection
                    //System.err.println("ssl matching " + read_buffer.position() + "/" + OneSwarmSslTransportHelperFilterStream.SSL_HEADER_MIN_LENGTH);
                    if (read_buffer.position() > OneSwarmSslTransportHelperFilterStream.SSL_HEADER_MIN_LENGTH) {
                        byte[] data = new byte[read_buffer.position()];
                        System.arraycopy(read_buffer.array(), 0, data, 0, data.length);
                        if (sslMatch == null) {
                            sslMatch = OneSwarmSslTransportHelperFilterStream.isSSLClientHello(data);
                        }
                        //System.out.println("ssl match: " + isClientHello.name());
                        if (sslMatch.equals(SslHandShakeMatch.SSL_CLIENT_CERT)
                                || sslMatch.equals(SslHandShakeMatch.SSL_NO_CLIENT_CERT)) {
                            selected_protocol = CRYPTO_SSL;

                            if (write_buffer == null) {
                                write_buffer = ByteBuffer
                                        .allocate(OneSwarmSslTransportHelperFilterStream.SSL_APP_BUFFER_SIZE);
                            }

                            read_buffer = ByteBuffer
                                    .allocate(OneSwarmSslTransportHelperFilterStream.SSL_NET_BUFFER_SIZE);
                            read_buffer.put(data);

                            sslFilter = new OneSwarmSslTransportHelperFilterStream(transport, outbound, sslMatch);
                            sslFilter.doHandshake(read_buffer, write_buffer);
                            transport.resumeReadSelects();
                            transport.resumeWriteSelects();

                            continue;
                        }
                    }
                    if (!read_buffer.hasRemaining()) {
                        // *********************************************************

                        read_buffer.flip();

                        byte[] other_dh_public_key_bytes = new byte[read_buffer.remaining()];

                        read_buffer.get(other_dh_public_key_bytes);

                        completeDH(other_dh_public_key_bytes);

                        read_buffer = null;

                        protocol_state = PS_OUTBOUND_2;
                    }

                } else if (protocol_state == PS_OUTBOUND_2) {

                    // B->A: Yb PadB

                    if (write_buffer == null) {

                        byte[] padding_b = getRandomPadding(getPaddingMax() / 2);

                        write_buffer = ByteBuffer.allocate(dh_public_key_bytes.length + padding_b.length);

                        write_buffer.put(dh_public_key_bytes);

                        write_buffer.put(padding_b);

                        write_buffer.flip();
                    }

                    write(write_buffer);

                    if (!write_buffer.hasRemaining()) {

                        write_buffer = null;

                        protocol_state = PS_INBOUND_3;
                    }

                } else if (protocol_state == PS_INBOUND_2) {

                    // A receives: Yb

                    if (read_buffer == null) {

                        read_buffer = ByteBuffer.allocate(dh_public_key_bytes.length);
                    }

                    read(read_buffer);

                    if (!read_buffer.hasRemaining()) {

                        read_buffer.flip();

                        byte[] other_dh_public_key_bytes = new byte[read_buffer.remaining()];

                        read_buffer.get(other_dh_public_key_bytes);

                        completeDH(other_dh_public_key_bytes);

                        // A initiates SKEY so we can now set up crypto

                        setupCrypto();

                        read_buffer = null;

                        protocol_state = PS_OUTBOUND_3;
                    }

                } else if (protocol_state == PS_OUTBOUND_3) {

                    // A->B: HASH('req1', S), HASH('req2', SKEY)^HASH('req3', S), ENCRYPT(VC, crypto_provide, len(PadC), PadC, len(IA)), ENCRYPT(IA)

                    if (write_buffer == null) {

                        int initial_data_out_len = initial_data_out == null ? 0 : initial_data_out.remaining();

                        // padding_a here is half of the padding from before

                        int pad_max = getPaddingMax();

                        byte[] padding_a = getRandomPadding(pad_max / 2);

                        byte[] padding_c = getZeroPadding(pad_max);

                        write_buffer = ByteBuffer.allocate(padding_a.length + 20 + 20
                                + (VC.length + 4 + 2 + padding_c.length + 2) + initial_data_out_len);

                        write_buffer.put(padding_a);

                        // HASH('req1', S)

                        SHA1Hasher hasher = new SHA1Hasher();

                        hasher.update(REQ1_IV);
                        hasher.update(secret_bytes);

                        byte[] sha1 = hasher.getDigest();

                        write_buffer.put(sha1);

                        // HASH('req2', SKEY)^HASH('req3', S)

                        hasher = new SHA1Hasher();

                        hasher.update(REQ2_IV);
                        hasher.update(shared_secret);

                        byte[] sha1_1 = hasher.getDigest();

                        hasher = new SHA1Hasher();

                        hasher.update(REQ3_IV);
                        hasher.update(secret_bytes);

                        byte[] sha1_2 = hasher.getDigest();

                        for (int i = 0; i < sha1_1.length; i++) {

                            sha1_1[i] ^= sha1_2[i];
                        }

                        write_buffer.put(sha1_1);

                        // ENCRYPT(VC, crypto_provide, len(PadC), PadC, len(IA)

                        write_buffer.put(write_cipher.update(VC));

                        write_buffer.put(write_cipher.update(new byte[] { 0, 0, 0, my_supported_protocols }));

                        write_buffer.put(write_cipher
                                .update(new byte[] { (byte) (padding_c.length >> 8), (byte) padding_c.length }));

                        write_buffer.put(write_cipher.update(padding_c));

                        write_buffer.put(write_cipher.update(
                                new byte[] { (byte) (initial_data_out_len >> 8), (byte) initial_data_out_len }));

                        if (initial_data_out_len > 0) {

                            int save_pos = initial_data_out.position();

                            write_cipher.update(initial_data_out, write_buffer);

                            // reset in case buffer needs to be used again by caller

                            initial_data_out.position(save_pos);

                            initial_data_out = null;
                        }

                        write_buffer.flip();
                    }

                    write(write_buffer);

                    if (!write_buffer.hasRemaining()) {

                        write_buffer = null;

                        protocol_state = PS_INBOUND_4;
                    }

                } else if (protocol_state == PS_INBOUND_3) {

                    // B receives: HASH('req1', S), HASH('req2', SKEY)^HASH('req3', S), ENCRYPT(VC, crypto_provide, len(PadC), PadC, len(IA)), ENCRYPT(IA)

                    if (read_buffer == null) {

                        read_buffer = ByteBuffer.allocate(20 + PADDING_MAX);

                        read_buffer.limit(20);

                        SHA1Hasher hasher = new SHA1Hasher();

                        hasher.update(REQ1_IV);
                        hasher.update(secret_bytes);

                        padding_skip_marker = hasher.getDigest();

                        protocol_substate = 1;
                    }

                    while (true) {

                        read(read_buffer);

                        if (read_buffer.hasRemaining()) {

                            break;
                        }

                        if (protocol_substate == 1) {

                            //skip up to HASH('req1', S)

                            int limit = read_buffer.limit();

                            read_buffer.position(limit - 20);

                            boolean match = true;

                            for (int i = 0; i < 20; i++) {

                                if (read_buffer.get() != padding_skip_marker[i]) {

                                    match = false;

                                    break;
                                }
                            }

                            if (match) {

                                read_buffer = ByteBuffer.allocate(20 + VC.length + 4 + 2);

                                protocol_substate = 2;

                                break;

                            } else {

                                if (limit == read_buffer.capacity()) {

                                    throw (new IOException("PHE skip to SHA1 marker failed"));
                                }

                                read_buffer.limit(limit + 1);

                                read_buffer.position(limit);
                            }
                        } else if (protocol_substate == 2) {

                            // find SKEY using HASH('req2', SKEY)^HASH('req3', S)  , ENCRYPT(VC, crypto_provide, len(PadC),

                            read_buffer.flip();

                            final byte[] decode = new byte[20];

                            read_buffer.get(decode);

                            SHA1Hasher hasher = new SHA1Hasher();

                            hasher.update(REQ3_IV);
                            hasher.update(secret_bytes);

                            byte[] sha1 = hasher.getDigest();

                            for (int i = 0; i < decode.length; i++) {

                                decode[i] ^= sha1[i];
                            }

                            synchronized (global_shared_secrets) {

                                shared_secret = (byte[]) global_shared_secrets.get(new HashWrapper(decode));
                            }

                            if (shared_secret == null) {

                                throw (new IOException("No matching shared secret"));
                            }

                            // System.out.println( "inbound - using crypto secret " + ByteFormatter.encodeString( shared_secret ));

                            setupCrypto();

                            byte[] crypted = new byte[VC.length + 4 + 2];

                            read_buffer.get(crypted);

                            byte[] plain = read_cipher.update(crypted);

                            byte other_supported_protocols = plain[VC.length + 3];

                            int common_protocols = my_supported_protocols & other_supported_protocols;

                            if ((common_protocols & CRYPTO_PLAIN) != 0) {

                                selected_protocol = CRYPTO_PLAIN;

                            } else if ((common_protocols & CRYPTO_XOR) != 0) {

                                selected_protocol = CRYPTO_XOR;

                            } else if ((common_protocols & CRYPTO_RC4) != 0) {

                                selected_protocol = CRYPTO_RC4;

                            } else if ((common_protocols & CRYPTO_AES) != 0) {

                                selected_protocol = CRYPTO_AES;

                            } else {

                                throw (new IOException("No crypto protocol in common: mine = "
                                        + Integer.toHexString((byte) my_supported_protocols) + ", theirs = "
                                        + Integer.toHexString((byte) other_supported_protocols)));

                            }

                            int padding = ((plain[VC.length + 4] & 0xff) << 8) + (plain[VC.length + 5] & 0xff);

                            if (padding > PADDING_MAX) {

                                throw (new IOException("Invalid padding '" + padding + "'"));
                            }

                            read_buffer = ByteBuffer.allocate(padding + 2);

                            // skip the padding

                            protocol_substate = 3;

                        } else if (protocol_substate == 3) {

                            // ENCRYPT( len(IA)),  { ENCRYPT(IA) }

                            read_buffer.flip();

                            byte[] data = new byte[read_buffer.remaining()];

                            read_buffer.get(data);

                            data = read_cipher.update(data);

                            int ia_len = 0xffff
                                    & (((data[data.length - 2] & 0xff) << 8) + (data[data.length - 1] & 0xff));

                            if (ia_len > 65535) {

                                throw (new IOException("Invalid IA length '" + ia_len + "'"));
                            }

                            if (ia_len > 0) {

                                read_buffer = ByteBuffer.allocate(ia_len);

                                // skip the padding

                                protocol_substate = 4;

                            } else {

                                read_buffer = null;

                                protocol_state = PS_OUTBOUND_4;

                                break;
                            }
                        } else if (protocol_substate == 4) {

                            // ENCRYPT(IA)

                            read_buffer.flip();

                            byte[] data = new byte[read_buffer.remaining()];

                            read_buffer.get(data);

                            data = read_cipher.update(data);

                            // hack alert - we can delay the writing of the outbound_4 packet if this is an incoming packet with
                            // a piggybacked bt handshake as we know that we'll be sending our own handshake back out pretty soon
                            // and it'll take the delayed data with it. To be more generic we'd need to add a callback to the pattern
                            // matcher to allow it to decide whether delaying was sensible / or stick a timer on the delayed data

                            delay_outbound_4 = new String(data).indexOf("BitTorrent") != -1;

                            // System.out.println( "Initial Data In: " + new String( data ) + "->delay=" +delay_outbound_4 );

                            initial_data_in = ByteBuffer.wrap(data);

                            read_buffer = null;

                            protocol_state = PS_OUTBOUND_4;

                            break;
                        }
                    }
                } else if (protocol_state == PS_OUTBOUND_4) {

                    // B->A: ENCRYPT(VC, crypto_select, len(padD), padD, // len(IB)), ENCRYPT(IB)

                    if (write_buffer == null) {

                        int pad_max = getPaddingMax();

                        byte[] padding_b = getRandomPadding(pad_max / 2); // half padding b sent here

                        byte[] padding_d = getZeroPadding(pad_max);

                        write_buffer = ByteBuffer.allocate(padding_b.length + VC.length + 4 + 2 + padding_d.length); // + 2 + initial_data_out.length );

                        write_buffer.put(padding_b);

                        write_buffer.put(write_cipher.update(VC));

                        write_buffer.put(write_cipher.update(new byte[] { 0, 0, 0, selected_protocol }));

                        write_buffer.put(write_cipher
                                .update(new byte[] { (byte) (padding_d.length >> 8), (byte) padding_d.length }));

                        write_buffer.put(write_cipher.update(padding_d));

                        //write_buffer.put( write_cipher.update( new byte[]{ (byte)(initial_data_out.length>>8),(byte)initial_data_out.length }));

                        //write_buffer.put( write_cipher.update( initial_data_out ));

                        write_buffer.flip();
                    }

                    if (delay_outbound_4) {

                        if (transport.delayWrite(write_buffer)) {

                            write_buffer = null;

                            handshakeComplete();

                        } else {

                            delay_outbound_4 = false;
                        }
                    }

                    if (!delay_outbound_4) {

                        write(write_buffer);

                        if (!write_buffer.hasRemaining()) {

                            write_buffer = null;

                            handshakeComplete();
                        }
                    }
                } else if (protocol_state == PS_INBOUND_4) {

                    // B->A: ENCRYPT(VC, crypto_select, len(padD), padD // , len(IB)), ENCRYPT(IB)

                    if (read_buffer == null) {

                        read_buffer = ByteBuffer.allocate(VC.length + PADDING_MAX);

                        read_buffer.limit(VC.length);

                        padding_skip_marker = new byte[VC.length];

                        padding_skip_marker = read_cipher.update(padding_skip_marker);

                        protocol_substate = 1;
                    }

                    while (true) {

                        read(read_buffer);

                        if (read_buffer.hasRemaining()) {

                            break;
                        }

                        if (protocol_substate == 1) {

                            //skip up to marker

                            int limit = read_buffer.limit();

                            read_buffer.position(limit - VC.length);

                            boolean match = true;

                            for (int i = 0; i < VC.length; i++) {

                                if (read_buffer.get() != padding_skip_marker[i]) {

                                    match = false;

                                    break;
                                }
                            }

                            if (match) {

                                read_buffer = ByteBuffer.allocate(4 + 2);

                                protocol_substate = 2;

                                break;

                            } else {

                                if (limit == read_buffer.capacity()) {

                                    throw (new IOException("PHE skip to SHA1 marker failed"));
                                }

                                read_buffer.limit(limit + 1);

                                read_buffer.position(limit);
                            }
                        } else if (protocol_substate == 2) {

                            //  ENCRYPT( crypto_select, len(padD))

                            read_buffer.flip();

                            byte[] crypted = new byte[4 + 2];

                            read_buffer.get(crypted);

                            byte[] plain = read_cipher.update(crypted);

                            selected_protocol = plain[3];

                            if ((selected_protocol & my_supported_protocols) == 0) {

                                throw (new IOException("Selected protocol has nothing in common: mine = "
                                        + Integer.toHexString((byte) my_supported_protocols) + ", theirs = "
                                        + Integer.toHexString((byte) selected_protocol)));

                            }

                            int pad_len = 0xffff & (((plain[4] & 0xff) << 8) + (plain[5] & 0xff));

                            if (pad_len > 65535) {

                                throw (new IOException("Invalid pad length '" + pad_len + "'"));
                            }

                            read_buffer = ByteBuffer.allocate(pad_len); // + 2 );

                            protocol_substate = 3;

                        } else if (protocol_substate == 3) {

                            read_buffer.flip();

                            byte[] data = new byte[read_buffer.remaining()];

                            read_buffer.get(data);

                            data = read_cipher.update(data);

                            handshakeComplete();

                            read_buffer = null;

                            break;
                            /*
                            int   ib_len   = 0xffff & ((( data[data.length-2] & 0xff ) << 8 ) + ( data[data.length-1] & 0xff ));
                                
                            if ( ib_len > 65535 ){
                                   
                               throw( new IOException( "Invalid IB length '" + ib_len + "'" ));
                            }
                                
                            read_buffer = ByteBuffer.allocate( ib_len );
                                
                            protocol_substate   = 4;
                                
                            }else{
                                
                            read_buffer.flip();
                                
                            byte[]   data = new byte[read_buffer.remaining()];
                                
                            read_buffer.get( data );
                                
                            initial_data_in = read_cipher.update( data );      
                                
                            handshakeComplete();
                                
                            read_buffer   = null;
                                
                            break;
                            */
                        }
                    }
                }

                if (handshake_complete) {

                    transport.cancelReadSelects();

                    transport.cancelWriteSelects();

                    loop = false;

                    complete();

                } else {

                    if (read_buffer == null) {

                        transport.pauseReadSelects();

                    } else {

                        transport.resumeReadSelects();

                        loop = false;

                    }

                    if (write_buffer == null) {

                        transport.pauseWriteSelects();

                    } else {

                        transport.resumeWriteSelects();

                        loop = false;
                    }
                }
            }
        } catch (Throwable e) {
            e.printStackTrace();
            failed(e);

            if (e instanceof IOException) {

                throw ((IOException) e);

            } else {

                throw (new IOException(Debug.getNestedExceptionMessage(e)));
            }
        } finally {

            process_mon.exit();
        }
    }

    /*
     **** OUTBOUND_1
         
       A sends B odd/even byte + Ya + Pa
           
     **** INBOUND_1
         
       B receives Ya
       B computes Yb
       B computes S and HS
           
       **** OUTBOUND_2
           
       B sends A Yb + HS( "supported methods" + len(Pb)) + Pb
           
     **** INBOUND_2
         
       A receives Yb
       A computes S and HS
       A receives HS( "supported methods" + len(Pb)) and decrypts using HS
       A skips len(Pb) random bytes
           
       **** OUTBOUND_3
           
       A sends SHA1(S) + HS( "selected method" + len(Pc)) + Pc + selectedCrypt( payload )
           
     **** INBOUND_3
         
       B skips Pa bytes until receives SHA1(S)
       B decrypts "selected method" + len(Pc) and skips len(Pc) bytes to get to selectedCrypt( payload... )
       B sends A selectedCrypt( payload... )
     */

    /*
    protected void
    process()
        
       throws IOException
    {
       try{
     process_mon.enter();
         
     if ( handshake_complete ){
            
        Debug.out( "Handshake process already completed" );
            
        return;
     }
         
     boolean   loop = true;
           
     while( loop ){
               
        if ( protocol_state == PS_OUTBOUND_1 ){
               
           if ( write_buffer == null ){
                  
                 // A sends B odd/even Ya + Pa
                  
                  
              byte[]   padding = getPadding();
                                    
              write_buffer = ByteBuffer.allocate( dh_public_key_bytes.length + padding.length );
                                    
              write_buffer.put( dh_public_key_bytes );
                  
              write_buffer.put( padding );
                  
              write_buffer.flip();
           }
               
           write( write_buffer );
                  
           if ( !write_buffer.hasRemaining()){
               
              write_buffer   = null;
               
              protocol_state   = PS_INBOUND_2;
           }
               
        }else if ( protocol_state == PS_OUTBOUND_2 ){
               
              // B sends A Yb + HS( "supported methods" + len(Pb)) + Pb
           
           if ( write_buffer == null ){
                  
              byte[]   padding = getPadding();
                  
              write_buffer = ByteBuffer.allocate( dh_public_key_bytes.length + 4 + 2 + padding.length );
                  
              write_buffer.put( dh_public_key_bytes );
                                          
                 // 4 bytes for my supported protocols
                                 
              write_buffer.put( write_cipher.update( new byte[]{ 0, 0, 0, my_supported_protocols }));
                  
              write_buffer.put( write_cipher.update( new byte[]{ (byte)(padding.length>>8),(byte)padding.length }));
               
              write_buffer.put( padding );
                  
              write_buffer.flip();
           }
               
           write( write_buffer );
               
           if ( !write_buffer.hasRemaining()){
               
              write_buffer   = null;
               
              protocol_state   = PS_INBOUND_3;
           }
               
        }else if ( protocol_state == PS_OUTBOUND_3 ){
               
              //    A sends SHA1(S) + HS( "selected method" + len(Pc)) + Pc + selectedCrypt( payload )
           
           if ( write_buffer == null ){
                  
              byte[]   padding = getPadding();
                  
              write_buffer = ByteBuffer.allocate( 20 + 4 + 2 + padding.length );
                  
              write_buffer.put( sha1_secret_bytes );
                                          
              write_buffer.put( write_cipher.update( new byte[]{ 0, 0, 0, selected_protocol }));
                  
              write_buffer.put( write_cipher.update( new byte[]{ (byte)(padding.length>>8),(byte)padding.length }));
               
              write_buffer.put( padding );
                  
              write_buffer.flip();
           }
               
           write( write_buffer );
               
           if ( !write_buffer.hasRemaining()){
               
              write_buffer   = null;
               
              handshakeComplete();
           }
               
        }else if ( protocol_state == PS_INBOUND_1 ){
                     
              // B receives marker + Ya
           
           read( read_buffer );
                     
           if ( !read_buffer.hasRemaining()){
                                 
              read_buffer.flip();
                  
              byte[] other_dh_public_key_bytes = new byte[read_buffer.remaining()];
                  
              read_buffer.get( other_dh_public_key_bytes );
         
              completeDH( other_dh_public_key_bytes );
                           
                read_buffer   = null;
                                  
              protocol_state   = PS_OUTBOUND_2;
           }
                  
        }else if ( protocol_state == PS_INBOUND_2 ){
                  
           //A receives Yb
           //A computes S and HS
           //A receives HS( "supported methods" + len(Pb)) and decrypts using HS
           //A skips len(Pb) random bytes
                  
           if ( read_buffer == null ){
                                    
              read_buffer = ByteBuffer.allocate( dh_public_key_bytes.length + 6 );
                  
              protocol_substate   = 1;
           }               
                  
           while( true ){
                  
              read( read_buffer );
                        
              if ( read_buffer.hasRemaining()){
                     
                 break;
              }
                  
              if ( protocol_substate == 1 ){
                     
                 read_buffer.flip();
                     
                 byte[] other_dh_public_key_bytes_etc = read_buffer.array();
         
                 completeDH( other_dh_public_key_bytes_etc );
         
                 byte[]   etc = read_cipher.update( other_dh_public_key_bytes_etc, DH_SIZE_BYTES, 6 );
                     
                 byte   other_supported_protocols = etc[3];
                     
                 int   common_protocols = my_supported_protocols & other_supported_protocols;
                     
                 if (( common_protocols & CRYPTO_PLAIN )!= 0 ){
                        
                    selected_protocol = CRYPTO_PLAIN;
                        
                 }else if (( common_protocols & CRYPTO_XOR )!= 0 ){
                        
                    selected_protocol = CRYPTO_XOR;
                        
                 }else if (( common_protocols & CRYPTO_RC4 )!= 0 ){
                        
                    selected_protocol = CRYPTO_RC4;
                        
                 }else if (( common_protocols & CRYPTO_AES )!= 0 ){
                        
                    selected_protocol = CRYPTO_AES;
                        
                 }else{
                        
                    throw( new IOException( 
                          "No crypto protocol in common: mine = " + 
                             Integer.toHexString((byte)my_supported_protocols) + ", theirs = " +
                             Integer.toHexString((byte)other_supported_protocols)));
            
                 }
                        
                 int   padding   = (( etc[4] & 0xff ) << 8 ) + ( etc[5] & 0xff );
                     
                 if ( padding > PADDING_MAX ){
                        
                    throw( new IOException( "Invalid padding '" + padding + "'" ));
                 }
                     
                 read_buffer = ByteBuffer.allocate( padding );
                     
                 protocol_substate   = 2;
                     
              }else{
                     
                 read_buffer   = null;
                                      
                 protocol_state   = PS_OUTBOUND_3;
                     
                 break;
              }
           }
        }else if ( protocol_state == PS_INBOUND_3 ){
               
               
           //   B skips Pa bytes until receives SHA1(S)
           //   B decrypts "selected method" + len(Pc) and skips len(Pc) bytes 
                  
           if ( read_buffer == null ){
                                    
              read_buffer = ByteBuffer.allocate( 20 + PADDING_MAX );
                  
              read_buffer.limit( 20 );
                  
              protocol_substate   = 1;
           }               
               
           while( true ){
                  
              read( read_buffer );
                        
              if ( read_buffer.hasRemaining()){
                  
                 break;
              }
                  
              if ( protocol_substate == 1 ){
                     
                 int   limit = read_buffer.limit();
                     
                 read_buffer.position( limit - 20 );
                     
                 boolean match   = true;
                     
                 for (int i=0;i<20;i++){
                        
                    if ( read_buffer.get() != sha1_secret_bytes[i] ){
                           
                       match   = false;
                           
                       break;
                    }
                 }
                     
                 if ( match ){
                     
                    read_buffer = ByteBuffer.allocate( 6 );
                        
                    protocol_substate   = 2;
                        
                    break;
                     
                 }else{
                        
                    if ( limit == read_buffer.capacity()){
                           
                       throw( new IOException( "PHE skip to SHA1 marker failed" ));
                    }
                        
                    read_buffer.limit( limit + 1 );
                        
                    read_buffer.position( limit );
                 }
              }else if ( protocol_substate == 2 ){
                     
                 read_buffer.flip();
                        
                 byte[]   etc = read_cipher.update( read_buffer.array());
                     
                 selected_protocol = etc[3];
                     
                 int   padding   = (( etc[4] & 0xff ) << 8 ) + ( etc[5] & 0xff );
                     
                 if ( padding > PADDING_MAX ){
                        
                    throw( new IOException( "Invalid padding '" + padding + "'" ));
                 }
        
                 read_buffer = ByteBuffer.allocate( padding );
                     
                 protocol_substate   = 3;
                                          
              }else{
                     
                 read_buffer   = null;
                    
                 handshakeComplete();
                     
                 break;
              }
           }
        }
           
        if ( handshake_complete ){
               
           read_selector.cancel( channel );
               
           write_selector.cancel( channel );
               
           loop   = false;
               
           complete();
               
        }else{
            
           if ( read_buffer == null ){
                  
              read_selector.pauseSelects( channel );
                  
           }else{
                  
              read_selector.resumeSelects ( channel );
                  
              loop   = false;
                  
           }
               
           if ( write_buffer == null ){
                  
              write_selector.pauseSelects( channel );
                  
           }else{
                  
              write_selector.resumeSelects ( channel );
                  
              loop   = false;
           }
        }
     }
       }catch( Throwable e ){
                  
     failed( e );
         
     if ( e instanceof IOException ){
            
        throw((IOException)e);
            
     }else{
            
        throw( new IOException( Debug.getNestedExceptionMessage(e)));
     }
       }finally{
         
     process_mon.exit();
       }
    }
    */

    protected void read(ByteBuffer buffer)

            throws IOException {
        int len = transport.read(buffer);

        //System.out.println( "read:" + this + "/" + protocol_state + "/" + protocol_substate + " -> " + len +"[" + buffer +"]");

        if (len < 0) {

            throw (new IOException("end of stream on socket read - phe: " + getString()));
        }

        bytes_read += len;
    }

    protected void write(ByteBuffer buffer)

            throws IOException {
        //System.out.println( "write pre:" + this + "/" + protocol_state + "/" + protocol_substate + " - " + buffer );

        int len = transport.write(buffer, false);

        //System.out.println( "write:" + this + "/" + protocol_state + "/" + protocol_substate + " -> " + len +"[" + buffer +"]");

        if (len < 0) {

            throw (new IOException("bytes written < 0 "));
        }

        bytes_written += len;
    }

    public boolean selectSuccess(TransportHelper transport, Object attachment, boolean write_operation) {
        try {
            int old_bytes_read = bytes_read;
            int old_bytes_written = bytes_written;

            process();

            if (write_operation) {

                return (bytes_written != old_bytes_written);

            } else {

                boolean progress = bytes_read != old_bytes_read;

                if (progress) {

                    last_read_time = SystemTime.getCurrentTime();
                }

                return (progress);
            }

        } catch (Throwable e) {

            failed(e);

            return (false);
        }
    }

    public void selectFailure(TransportHelper transport, Object attachment, Throwable msg) {
        failed(msg);
    }

    protected byte[] bigIntegerToBytes(BigInteger bi, int num_bytes) {
        String str = bi.toString(16);

        while (str.length() < num_bytes * 2) {
            str = "0" + str;
        }

        return (ByteFormatter.decodeString(str));
    }

    protected BigInteger bytesToBigInteger(byte[] bytes, int offset, int len) {
        return (new BigInteger(ByteFormatter.encodeString(bytes, offset, len), 16));
    }

    protected int getPaddingMax() {
        if (transport.minimiseOverheads()) {

            return (PADDING_MAX_LIMITED);

        } else {

            return (PADDING_MAX_NORMAL);
        }
    }

    protected static synchronized byte[] getRandomPadding(int max_len) {
        byte[] bytes = new byte[random.nextInt(max_len)];

        random.nextBytes(bytes);

        return (bytes);
    }

    protected static synchronized byte[] getZeroPadding(int max_len) {
        byte[] bytes = new byte[random.nextInt(max_len)];

        return (bytes);
    }

    protected static KeyPair generateDHKeyPair(TransportHelper transport, boolean outbound)

            throws IOException {
        synchronized (dh_key_generator) {

            if (!outbound) {

                byte[] address = transport.getAddress().getAddress().getAddress();

                int hit_count = generate_bloom.add(address);

                long now = SystemTime.getCurrentTime();

                // allow up to 10% bloom filter utilisation

                if (generate_bloom.getSize() / generate_bloom.getEntryCount() < 10) {

                    generate_bloom = BloomFilterFactory
                            .createAddRemove4Bit(generate_bloom.getSize() + BLOOM_INCREASE);

                    generate_bloom_create_time = now;

                    Logger.log(new LogEvent(LOGID, "PHE bloom: size increased to " + generate_bloom.getSize()));

                } else if (now < generate_bloom_create_time || now - generate_bloom_create_time > BLOOM_RECREATE) {

                    generate_bloom = BloomFilterFactory.createAddRemove4Bit(generate_bloom.getSize());

                    generate_bloom_create_time = now;
                }

                if (hit_count >= 60) {

                    Logger.log(new LogEvent(LOGID,
                            "PHE bloom: too many recent connection attempts from " + transport.getAddress()));

                    throw (new IOException("Too many recent connection attempts (phe)"));
                }

                long since_last = now - last_dh_incoming_key_generate;

                long delay = 100 - since_last;

                // limit key gen operations to 10 a second

                if (delay > 0 && delay < 100) {

                    try {
                        Thread.sleep(delay);

                    } catch (Throwable e) {
                    }
                }

                last_dh_incoming_key_generate = now;
            }

            KeyPair res = dh_key_generator.generateKeyPair();

            return (res);
        }
    }

    protected void complete() {
        // System.out.println( (outbound?"out: ":"in :") + " complete, r " + bytes_read + ", w " + bytes_written + ", initial data = " + initial_data_in.length + "/" + initial_data_out.length );

        processing_complete = true;

        adapter.decodeComplete(this, initial_data_out);
    }

    protected void failed(Throwable cause) {
        // System.out.println( (outbound?"out: ":"in :") + " failed, " + cause.getMessage());

        processing_complete = true;

        transport.cancelReadSelects();

        transport.cancelWriteSelects();

        adapter.decodeFailed(this, cause);
    }

    public boolean isComplete(long now) {
        return (processing_complete);
    }

    public TransportHelperFilter getFilter() {
        return (filter);
    }

    public long getLastReadTime() {
        long now = SystemTime.getCurrentTime();

        if (last_read_time > now) {

            last_read_time = now;
        }

        return (last_read_time);
    }

    public String getString() {
        return ("state=" + protocol_state + ",sub=" + protocol_substate + ",in=" + bytes_read + ",out="
                + bytes_written);
    }
}