/*
 * Created on 17-Jan-2006
 * Created by Paul Gardner
 * Copyright (C) Azureus Software, Inc, 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.
 *
 */

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

import java.io.IOException;
import java.math.BigInteger;
import java.net.InetSocketAddress;
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.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.AddressUtils;
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.LightHashMap;
import org.gudy.azureus2.core3.util.RandomUtils;
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.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 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_setup_done;
	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 Random	random = RandomUtils.SECURE_RANDOM;
    
	private static Map	global_shared_secrets	= new LightHashMap();
	
	private static boolean
	cryptoSetup()
	{
		synchronized( global_shared_secrets ){
			
			if ( crypto_setup_done ){
				
				return( crypto_ok );
			}
			
			crypto_setup_done = true;
			
			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;
			}
			
			return( crypto_ok );
		}
	}
	
	public static boolean
	isCryptoOK()
	{
		return( cryptoSetup());
	}
	
	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 );

	
	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;
	    					 
	    				 }else if ( min.equals( "RC4" )){
	    					 
	    					 MIN_CRYPTO	= CRYPTO_RC4 | CRYPTO_AES;
	    					 
	    				 }else if ( min.equals( "AES" )){
	    					
	    					 MIN_CRYPTO	= CRYPTO_AES;
	    					 
	    				 }else{
	    					 
	    					 MIN_CRYPTO	= CRYPTO_PLAIN | CRYPTO_XOR | CRYPTO_RC4 | CRYPTO_AES;
	    				 } 
	    				 
	    				 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" );
	
	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;
		
		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;
			}
		}
		
		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;
	
				read_buffer = ByteBuffer.allocate( dh_public_key_bytes.length );					
					
				read_buffer.put( _header );
			
				bytes_read += _header.limit();
			}
		}finally{
			
			process_mon.exit();
		}
		
		process();
	}
	
	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 ){
			
			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_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.out.println( this + ":" + (outbound?"out: ":"in : ") + protocol_state + "/" + protocol_substate + ": r " + bytes_read + " - " + read_buffer + ", w " + bytes_written + " - " + write_buffer );
				
				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 );
						
					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 ){
						
			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
	{
		if ( dh_key_generator == null ){
			
			throw( new IOException( "Crypto not setup" ));
		}
		
		synchronized( dh_key_generator ){
			
			if ( !outbound ){
				
				InetSocketAddress is_address = transport.getAddress();
				
				byte[]	address = AddressUtils.getAddressBytes( is_address );
				
				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 >= 15 ){
					
		     		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;
		
		transport.setScatteringMode(0);
		
		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);
	}
}
