/****************************************************************************
 * 
 * Author: Justin Samuel <justin /at/ justinsamuel /dot/ com>
 * Date: 2008-04-25
 * License: GPL
 * 
 * DES key brute force code.
 * 
 * This file relies on:
 *  + descrack_util.js
 *  + descrack_ui.js
 *  
 ****************************************************************************/

var delay = 1; // milliseconds to pause between testing each byte
var failedRequestWait = 10000; // milliseconds to wait after a request fails before trying again

var key; // the key that is being tested currently
var byteRange; // number of bytes to test
var pt; // the target plaintext
var ct; // the target ciphertext
var keyNextByte6; // 6th byte of key to test next
var keyLastByte6; // 6th byte of key to test last
var keyNextByte7; // 7th byte of key to test next
var keyLastByte7; // 7th byte of key to test last

var online = true; // true if the client is participating in online tests
var httpRequest; // the current XMLHttpRequest object
var httpRequestQueryString; // the query string of the current XMLHttpRequest object
var clientVersion = '1.0'; // the communication api version of this client
var serverUrl; // url of directory on server where descrack application resides
var sessionId; // client's sessionId
var response; // parsed response from server 

var lastTime; // time before current byte being processed
var totalTime; // total amount of time spent processing keys
var bytesTested; // number of bytes (not keys) tested
var byteTimes; // an array of times spent processing each byte

var started = false;
var paused = false;

var keyFound = false;

function resetLocalStats() {
    date = new Date();
    lastTime = date.getTime();
    totalTime = 0;
    bytesTested = 0;
    byteTimes = new Array();
}

function recordByteProcessed() {
    var date = new Date();
    var curTime = date.getTime();
    var timeDifference = curTime - lastTime;
    bytesTested++;
    byteTimes[bytesTested] = timeDifference;
    totalTime += timeDifference;
    lastTime = curTime;
    updateStats();
}

function setStartKey(newStartKey) {
	key = newStartKey;
}

function setByteRange(newByteRange) {
	byteRange = newByteRange;
}

function setPlaintext(newPlaintext) {
	pt = newPlaintext;
}

function setCiphertext(newCiphertext) {
	ct = newCiphertext;
}

function setDelay(newDelay) {
    delay = newDelay;
}

/*
 * Do initial request for range of keys to check.
 */
function requestRange() {
    try {
    	commStatus("Requesting initial key range.");
    	httpRequest = createXHR(handleRangeResponse);
    	queryString = 'clientVersion=' + clientVersion;
    	var requestUrl = serverUrl + '/range/request?' + queryString;
    	log("Sending request: " + requestUrl);
        httpRequest.open('GET', requestUrl, true);
        httpRequest.send(null);	
    } catch (e) {
        handleException(e);
    }
}

/*
 * Reports results of checked range and will receive back next range to
 * check.
 */
function reportRange(queryString) {
    try {
    	commStatus("Reporting results and requesting next key range.");
    	httpRequest.abort();
    	httpRequest = createXHR(handleRangeResponse);
        var requestUrl = serverUrl + '/range/report?' + queryString;
        log("Sending request: " + requestUrl);
        httpRequest.open('GET', requestUrl, true);
        httpRequest.send(null);
    } catch (e) {
        handleException(e);
    }
}

/*
 * Handles the response received by either the initial range request or
 * subsequent range reports.
 */
function handleRangeResponse() {
	if (keyFound) {
		commStatus(false);
		return;
	}
	try {
        if (httpRequest.readyState == 4) {
            // the response has been received
        	commStatus(true);
            if (httpRequest.status == 200) {
            	response = JSON.parse(httpRequest.responseText);
            	if (typeof response === 'string') {
            		// we got a message string back rather than a response that tells us
            		// what keys to check, so we stop and display the message.
            		status("Server said: " + response);
            		commStatus(false);
            	} else {
            		// we'll modify key, but need the original key to report back with
            		key = response.key.slice();
            		
            		pt = response.plaintext;
            		ct = response.ciphertext;
            		byteRange = response.byteRange;
            		sessionId = response.sessionId;
            		
            		log("Raw response from server: " + httpRequest.responseText);
            		
            		commStatus(false);
            		testRange();
            	}
            } else {
            	status("Did not understand response from server. Trying again.");
            	commStatus(false);
            	log("Server status: " +  httpRequest.status);
            	log("Server response: " +  httpRequest.response);
            	
            	// try report again in 10 seconds
            	// this assumes the failure didn't happen on the initial request
            	setTimeout("reportRange(httpRequestQueryString)", failedRequestWait);
            }
        }
	} catch (e) {
		commStatus(false);
		status("Problem communicating with server. Trying again.");
        // try report again in 10 seconds
        // this assumes the failure didn't happen on the initial request
        setTimeout("reportRange(httpRequestQueryString)", failedRequestWait);
		
		handleException(e);
		if (DOMException && e.code == DOMException.INVALID_STATE_ERR) {
		    // Opera is wacky. And IE doesn't even have a DOMException, it seems.
			// This is exactly why one isn't supposed to code one's own xhr communication,
			// but use an ajax library instead.
		} else {
		    handleException(e);
		}
	}
}

/*
 * Starts of testing of a range.
 */
function testRange() {
    try {
    	if ((key[5] << 8) + key[6] + byteRange > 0x10000) {
    		throw new Error("Range size too large for given start key.");
    	}
    	spacer = "&nbsp;&nbsp;&nbsp;&nbsp;";
    	log("Beginning keyspace search using:");
    	log(spacer + "plaintext: " + pt);
    	log(spacer + "ciphertext: " + ct);
    	log(spacer + "start key: " + key);
    	log(spacer + "key range (bytes): " + byteRange);
        keyNextByte6 = key[5];
        keyLastByte6 = key[5] + (byteRange >>> 8);
        keyNextByte7 = key[6];
        keyLastByte7 = key[6] + (byteRange & 0xff) - 1;
        var lastKeyByteToTest = key.slice(0,7);
        lastKeyByteToTest[5] = keyLastByte6;
        lastKeyByteToTest[6] = keyLastByte7;
        log(spacer + "Last byte in range: " + lastKeyByteToTest + ",[0-255]");
    	testKeysInNextLastByte();
    } catch (e) {
        handleException(e);
    }
}

/*
 * Notify the server that the key wasn't found in the last range.
 */
function reportRangeKeyNotFound() {
    try {
    	if (online) {
            queryData = new Object();
            queryData['clientVersion'] = clientVersion;
            queryData['sessionId'] = sessionId;
            queryData['keyStatus'] = 'notfound';
            queryData['byte1'] = response.key[0];
            queryData['byte2'] = response.key[1];
            queryData['byte3'] = response.key[2];
            queryData['byte4'] = response.key[3];
            queryData['byte5'] = response.key[4];
            queryData['byte6'] = response.key[5];
            queryData['byte7'] = response.key[6];
        
            queryString = '';
            for (i in queryData) {
            	queryString += i + '=' + queryData[i] + '&';
            }
            
            // get a new range back as the response
            queryString += '&requestRange=1';
            
            // remember for repeating request in case of failure
            httpRequestQueryString = queryString;
            
        	reportRange(queryString);
        } else {
            status('Key not found.');
        }
    } catch (e) {
        handleException(e);
    }
}

/*
 * Notify the server that the key was found in the last range.
 */
function reportRangeKeyFound() {
    try {
    	if (online) {
            queryData = new Object();
            queryData['clientVersion'] = clientVersion;
            queryData['sessionId'] = sessionId;
            queryData['keyStatus'] = 'found';
            queryData['byte1'] = response.key[0];
            queryData['byte2'] = response.key[1];
            queryData['byte3'] = response.key[2];
            queryData['byte4'] = response.key[3];
            queryData['byte5'] = response.key[4];
            queryData['byte6'] = response.key[5];
            queryData['byte7'] = response.key[6];
            queryData['foundKeyByte1'] = key[0];
            queryData['foundKeyByte2'] = key[1];
            queryData['foundKeyByte3'] = key[2];
            queryData['foundKeyByte4'] = key[3];
            queryData['foundKeyByte5'] = key[4];
            queryData['foundKeyByte6'] = key[5];
            queryData['foundKeyByte7'] = key[6];
            queryData['foundKeyByte8'] = key[7];
        
            queryString = '';
            for (i in queryData) {
                queryString += i + '=' + queryData[i] + '&';
            }
        
            // remember for repeating request in case of failure
            httpRequestQueryString = queryString;
            
            reportRange(queryString);
        } else {
            var key_with_parity_bits = key.slice();
            des_setparity(key_with_parity_bits);
            var message = 'You found the key: ' + key + '<br />'
                        + 'Key in hex with parity bits set: <tt>'
                        + decByteToHexString(key_with_parity_bits) + "</tt><br />";
        	status(message);
        }
    } catch (e) {
        handleException(e);
    }
}

/*
 * Main function that starts the communication with the server and the brute force 
 * key testing.
 */
function desCrack() {
    try {
    	if (serverUrl == undefined) {
    		serverUrl = dirname(location.href);
    	}

    	resetLocalStats();
        
    	if (online) {
            requestRange();
    	} else {
    		testRange();
    	}
    } catch (e) {
        handleException(e);
    }
}

/*
 * This allows pausing of testing.
 */
function completedTestingKeysInLastByte() {
	if (paused) {
		// keep calling this same function as long as client is paused
		setTimeout('completedTestingKeysInLastByte()', 1000); // emulate sleep()
	} else {
		setTimeout('testKeysInNextLastByte()', delay); // emulate sleep()
	}
}

/*
 * Tests all of the keys in range covered by the last byte of a raw 64-bit key.
 * The skips keys that only differ by the parity bit and ultimately tests 127 
 * keys ranging from 0x------00 to 0x------fe. 
 * 
 * After a final byte range is fully tested, it sets itself to execute again
 * after a delay so as not to lock up the browser by continue execution.
 *
 * The next byte tested will be the same final byte range but with the second
 * to last byte incremented by two (again, skipping keys that only differ by
 * the parity bit).
 */
function testKeysInNextLastByte() {
    try {
    	// local variables for javascript scope efficiency
        var loc_log = log;
        var loc_status = status;
        var loc_printTime = printTime;
        var loc_key = key;
    	
        loc_key[6] = keyNextByte7;
        
        var message = "Testing keys: " + loc_key.slice(0,7) + ",[0-255]";
        loc_log(message);
        loc_status(message);
        loc_printTime();
        
        // increment by two because to skip parity bit
        for (var i=0, resultCt, loc_ct=ct, loc_pt=pt, loc_equal=arraysAreEqual,
                 loc_des_encrypt=des_encrypt; i<256; i+=2) {
            loc_key[7] = i;
            resultCt = loc_pt.slice();
            loc_des_encrypt(loc_key, resultCt, 1);
            if (loc_equal(resultCt, loc_ct)) {
            	var message = "You found the key! " + loc_key;
                log(message);
                loc_status(message);
                loc_printTime();
                keyFound = true;
                reportRangeKeyFound();
                return;
            }
            // check if search is supposed to be aborted
            if (!started) {
            	return;
            }
        }
        
        recordByteProcessed();
        
        keyNextByte7 += 2; // increment by two because to skip parity bit
        if (keyNextByte7 > 0xff) {
            keyNextByte7 = 0x00;
            keyNextByte6 += 2;
            loc_key[5] = keyNextByte6;
        }
        
        if (keyNextByte6 > keyLastByte6 || (keyNextByte6 == keyLastByte6 && keyNextByte7 > keyLastByte7)) {
            log("Didn't find the key.");
            loc_status("Didn't find the key.");
            loc_printTime();
            reportRangeKeyNotFound();
            return;
        }
        
        completedTestingKeysInLastByte();
    } catch (e) {
        handleException(e);
    }
}
