// Entropy collector.
// Mouse position, keycodes and intervals between keystrokes are intended to be used.

// Von Neumann method (look at rfc4086) is used to deskew collected bits.
//  SHA256 is used to mix deskewed bits.

var BITS_PER_POINT = 8;
var BITS_PER_KEYCODE = 4;
var BITS_PER_KEYPRESS = 6;
var TRUST_RATIO = 3;

function pair(x, y)
{
    this.x = x;
    this.y = y;
}

function Entropy(numbits)
{
    this.mouse_entropy = new Hashtable(Math.round(numbits / 2), function(point) { return (point.x * 10000 + point.y); });
    this.keycodes_entropy = new Hashtable(Math.round(numbits / 2), function(keycode) { return keycode; });
    this.keypress_entropy = new Hashtable(Math.round(numbits / 2), function(interval) { return interval; });

    this.mouse_count = 0;
    this.keycode_count = 0;
    this.keypress_count = 0;

    this.total_count = 0;

    this.wanted_entropy = numbits;

    this.last_keypress = null;


    this.stir_mouse = Entropy_stir_mouse;
    this.stir_keycode = Entropy_stir_keycode;
    this.stir_keypress = Entropy_stir_keypress;
    this.enough_entropy = function() { return (this.total_count >= (TRUST_RATIO * numbits)); };
    this.entropy_collected = function() { return (this.total_count / TRUST_RATIO); };
    this.get_entropy = Entropy_get_entropy;
}

function Entropy_stir_mouse(x, y)
{
    var point = new pair(x, y);

    if (!this.mouse_entropy.has_value(point)) {
	this.mouse_entropy.insert(point);
	this.mouse_count += BITS_PER_POINT;
	this.total_count += BITS_PER_POINT;
    }
}

function Entropy_stir_keycode(code)
{
    if (!this.keycodes_entropy.has_value(code)) {
	this.keycodes_entropy.insert(code);
	this.keycode_count += BITS_PER_KEYCODE;
	this.total_count += BITS_PER_KEYCODE;
    }
}

function Entropy_stir_keypress()
{
    var date = new Date();
    var time = date.getTime();

    if (this.last_keypress == null) {
	this.last_keypress = time;
    }
    else {
	var interval = time - this.last_keypress;
	this.last_keypress = time;

	if (!this.keypress_entropy.has_value(interval)) {
	    this.keypress_entropy.insert(interval);
	    this.keypress_count += BITS_PER_KEYPRESS;
	    this.total_count += BITS_PER_KEYPRESS;
	}
    }
}

function neumann_deskew(array)
{
    var length = array.length;
    var byte_buffer = 0;
    var buffer = 0;
    var position = 0;
    var bits_count = 0;
    var result = new Array();
    var bit_to_write;
    var needs_writing;

    var deskew_bits = 2;
    var bits = new Array(deskew_bits);

    var bits_per_byte = 8;

    while (true) {
	for (var i = 0; i < deskew_bits; i++) {
	    if (buffer == 0) {
		if (position < length) {
		    buffer = array[position++];
		}
		else {
		    if (byte_buffer != 0) {
			result.push(byte_buffer);
		    }

		    return result;
		}
	    }

	    bits[i] = buffer & 0x1;
	    buffer >>= 1;
	}

	if ((bits[0] == 0) && (bits[1] == 1)) {
	    bit_to_write = 0;
	    needs_writing = true;
	}
	else if ((bits[0] == 1) && (bits[1] == 0)) {
	    bit_to_write = 1;
	    needs_writing = true;
	}
	else {
	    needs_writing = false;
	}

	if (needs_writing) {
	    if (bits_count >= bits_per_byte) {
		result.push(byte_buffer);
		byte_buffer = 0;
		bits_count = 0;
	    }

	    byte_buffer = (byte_buffer << 1) + bit_to_write;
	    ++bits_count;
	}
    }
}

function Entropy_get_entropy()
{
    if (this.enough_entropy()) {
	var sha256 = new SHA256();
	var hash_size = sha256.hash_size_bits();
	var chunks = Math.ceil(this.wanted_entropy / hash_size);

	var collected_entropy = new Array();
	collected_entropy = collected_entropy.concat(this.mouse_entropy.to_ids_array(), this.keycodes_entropy.to_ids_array(), this.keypress_entropy.to_ids_array());
	collected_entropy = neumann_deskew(collected_entropy);

	var length = collected_entropy.length;
	var chunk_size = Math.ceil(length / chunks);
	var curr_chunk_start = 0;
	var curr_chunk_end = chunk_size - 1;
	var resulting_entropy = new Array();

	for (var i = 0; i < chunks; i++) {
	    resulting_entropy = resulting_entropy.concat(sha256.digest(collected_entropy.slice(curr_chunk_start, curr_chunk_end + 1)));
	    curr_chunk_start = curr_chunk_end + 1;
	    curr_chunk_end = curr_chunk_end + chunk_size;
	}

	return resulting_entropy;
    }
    else {
	return null;
    }
}

