/*
 *  © Darren McDonald 2010
 *
 *  This file is part of athena-ssl-cipher-check.
 *
 *  athena-ssl-cipher-check 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 3 of the License,
 *  or (at your option) any later version.
 *
 *  athena-ssl-cipher-check 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 athena-ssl-cipher-check. If not, see
 *  <http://www.gnu.org/licenses/>.
 *
 */

package athenasslciphercheck;

import java.util.ArrayList;

public class tlsHeuristics {

    private ArrayList<Cipher> cipherDescList;
    private byte[] nonCipher = new byte[2];
    private boolean nonCipherAssigned = false;

    public int resetCount = 0;
    public int timedOutCount = 0;
    public int unidentifiedCount = 0;
    public int rejectedCount = 0;
    private CipherListWrapper cipherListWrapper;
    private Settings settings;


    public void checkTlsCiphers()
    {
        //build cipher byte array
        //    possible ciphers * cipher code size = array size
        //    256^2 * 2 = 131072
        byte[] cipherList = new byte[131072];

        //build cipher list
        int count = 0;
        for( int x = 0; x <= 0xFF; x++ )
        {
            for( int y = 0; y <= 0xFF; y++ )
            {
                cipherList[count] = (byte) x;
                count++;
                cipherList[count] = (byte) y;
                count++;
            }
        }

        //start searching
        System.out.println();
        System.out.println("SSLv3 Ciphers");

        //Rate limit should be a whole non-negative power of two, e.g. 1,2,4,8,16,32, etc
        //weird things happen otherwise!
        int rateLimit = 1;
        if( settings.getSafe() )
        {
            rateLimit = 32;
        }

        for( int i = 0x00; i <= (0xFF/32)*rateLimit; i++ )
        {
            this.search(cipherList, (512*i*32)/rateLimit, (512*32)/rateLimit, (byte) 0x00);
            if( timedOutCount > 5 )
            {
                System.out.println("SSlv3 being skipped due to timeouts");
                break;
            }
        }

        this.printStats();

        resetCount = 0;
        timedOutCount = 0;
        unidentifiedCount = 0;
        rejectedCount = 0;

        nonCipher[0] = (byte) 0xFF;
        nonCipher[1] = (byte) 0xFF;

        //Rebuild Cipher List
        count = 0;
        for( int x = 0; x <= 0xFF; x++ )
        {
            for( int y = 0; y <= 0xFF; y++ )
            {
                cipherList[count] = (byte) x;
                count++;
                cipherList[count] = (byte) y;
                count++;
            }
        }

        nonCipher[0] = (byte) 0xFF;
        nonCipher[1] = (byte) 0xFF;
        nonCipherAssigned = false;

        System.out.println();
        System.out.println("TLSv1 Ciphers");
        for( int i = 0x00; i <= (0xFF/32)*rateLimit; i++ )
        {
            this.search(cipherList, (512*i*32)/rateLimit, (512*32)/rateLimit, (byte) 0x01);
            if( timedOutCount > 5 )
            {
                System.out.println("TLSv1 being skipped due to timeouts");
                break;
            }            
        }

        this.printStats();
    }

    public void search( byte[] cipherList, int offset, int length, byte ver )
    {
        String proto = "";
        if( ver == 0x01 )
            proto = "TLSv1";
        else
            proto = "SSLv3";
        
        FoundCipher foundCipher = new FoundCipher();
        FoundCipher foundCipher2 = new FoundCipher();
        foundCipher.cipher = 0xFFFF;
        foundCipher2.cipher = 0xFFFF;


        int lowerResult = tlsCipherCheck.check(cipherList, offset, length/2, ver, foundCipher);
        int upperResult = tlsCipherCheck.check(cipherList, offset+(length/2), length/2, ver, foundCipher2);

        if( nonCipherAssigned == false )
        {
            if( lowerResult != 1 )
            {
                nonCipher[0] = cipherList[offset];
                nonCipher[1] = cipherList[offset+1];
                nonCipherAssigned = true;
            }
            else if( upperResult != 1 )
            {
                nonCipher[0] = cipherList[offset+(length/2)];
                nonCipher[1] = cipherList[offset+(length/2)+1];
                nonCipherAssigned = true;
            }
        }

        if( foundCipher.cipher != 0xFFFF &&
            lowerResult == 1)
        {
            cipherList[ foundCipher.cipher *2 ] = nonCipher[0];
            cipherList[ (foundCipher.cipher *2) +1 ] = nonCipher[1];

            byte[] tmp_a = new byte[2];
            tmp_a[0] = (byte) (foundCipher.cipher / 256);
            tmp_a[1] = (byte) (foundCipher.cipher % 256);

            Cipher c = cipherListWrapper.lookUpCipher( tmp_a );
            if( c != null)
                Results.addResultLine(c, proto);
            else
                Results.addResultLine( null, "UNKNOWN CIPHER #" + foundCipher.cipher );
        }

        if( foundCipher2.cipher != 0xFFFF &&
            upperResult == 1)
        {
            cipherList[ foundCipher2.cipher *2 ] = nonCipher[0];
            cipherList[ (foundCipher2.cipher *2) +1 ] = nonCipher[1];

            byte[] tmp_a = new byte[2];
            tmp_a[0] = (byte) (foundCipher2.cipher / 256);
            tmp_a[1] = (byte) (foundCipher2.cipher % 256);

            Cipher c = cipherListWrapper.lookUpCipher( tmp_a );
            if( c != null)
                Results.addResultLine(c, proto);
            else
                Results.addResultLine( null, "UNKNOWN CIPHER #" + foundCipher2.cipher );
        }

        updateCounts( lowerResult, upperResult );

        //if lower half of tree contains 1 cipher and was accepted
        if( length == 4 && lowerResult == 1 &&
            !( cipherList[ offset ] == nonCipher[0] && cipherList[ offset+1 ] == nonCipher[1] ) )
        {
            byte[] tmp = new byte[2];
            tmp[0] = cipherList[ offset ];
            tmp[1] = cipherList[ offset+1 ];
            
            Cipher c = cipherListWrapper.lookUpCipher(tmp);
            if( c != null)
                Results.addResultLine(c, proto);
            else
                Results.addResultLine( null, "UNKNOWN CIPHER #" + (offset/2) );
        }

        //This above section means this method of finding codes is no longer required
        // *KEEP THIS COMMENT JUST IN CASE*
        /*
        //if upper half of tree contains 1 cipher and was accepted
        if ( length == 4 && upperResult == 1 &&
            !( cipherList[ offset ] == nonCipher[0] && cipherList[ offset+1 ] == nonCipher[1] ))
        {
            //System.out.println("Found one!! "+( (offset+(length/2)) / 2) );
            byte[] tmp = new byte[2];
            tmp[0] = cipherList[ offset+(length/2) ];
            tmp[1] = cipherList[ offset+(length/2) +1 ];

            //Debug, Remove
            System.out.println( "" + cipherList[ offset ] + ", " + cipherList[ offset+1 ]);
            //End Debug

            Cipher c = cipherListWrapper.lookUpCipher(tmp);
            if( c != null)
                Results.addResultLine(c, proto);
            else
                Results.addResultLine( null, "UNKNOWN CIPHER #" + (offset+(length/4)) );

        }
        */

        //If not at the end of the tree, continue searching on positive branches
        if( length != 4 )
        {
            if( lowerResult == 1 )
            {
                this.search(cipherList, offset, length/2, ver );
            }
            if( upperResult == 1 )
            {
                this.search(cipherList, offset+(length/2), length/2, ver );
            }
        }

    }

    public tlsHeuristics( Settings s )
    {
        cipherListWrapper = new CipherListWrapper();
        cipherListWrapper.loadCiphers();

        settings = s;
    }

    /* The whole print status, and error reporting needs ,
     * over hauling. Convert to a debug mode maybe? */
    private void printStats()
    {
        if( /*resetCount == 0 &&*/
            timedOutCount == 0 &&
            unidentifiedCount == 0 /*&&
            rejectedCount == 0*/ )
            return;

        System.out.println("");
        boolean first = true;
        if( false /*resetCount > 0*/ )
        {
            System.out.print(resetCount + " connections reset");
            first = false;
        }

        if( timedOutCount > 0 )
        {
            if( !first )
                System.out.print(", ");
            System.out.print(timedOutCount + " connections timed out");
            first = false;
        }

        if( unidentifiedCount > 0 )
        {
            if( !first )
                System.out.print(", ");
            System.out.print(unidentifiedCount + " connections responded with unexpected data");
            first = false;
        }

        if( /*rejectedCount > 0*/ false )
        {
            if( !first )
                System.out.print(", ");
            System.out.print(rejectedCount + " ciphers sets rejected");
            first = false;
        }

        System.out.println();
    }

    private void updateCounts( int lowerResult, int upperResult)
    {
        if( lowerResult == 0 || lowerResult == -5 )
            resetCount++;
        if( upperResult == 0 || upperResult == -5  )
            resetCount++;

        if( lowerResult == -1 )
            rejectedCount++;
        if( upperResult == -1 )
            rejectedCount++;

        if( lowerResult == -2 )
            unidentifiedCount++;
        if( upperResult == -2 )
            unidentifiedCount++;

        if( lowerResult == -3 )
        {
            timedOutCount++;
        }
        if( upperResult == -3 )
        {
            timedOutCount++;
        }
    }
}
