Collecting entropy by human input in the browser using javascript

The idea

Some days ago i thought about the usual problems of password generation. One of them being that a generated password always can only be as good as the entropy used to generate it.

While in the past years entropy generators have improved, maybe the one thing that still may be the most unpredictable source is the human itself.

So how about collecting some entropy by observing the user's behaviour?

The idea grew so i ended up creating a proof of concept script which uses javascript to collect entropy by human input/interaction.

Let's do this

So whats the plan? We know we want to collect entropy by human input using javascript in the browser.

Since this is just a simple test script we won't use any fancy library but rather implement it good old fashioned vanilla.

When looking at the possible user defined things we can grab, there basically are two types of information:

Looking at the list of dynamic information, i decided to go for the mouse movement.

As some of you might know, there is also a very prominent tool using mouse movement as input for key generation which is called "puttygen". So i guess if it's good enough for them, its good enough for us too.

At all - this doesn't sound to hard right? When it comes to the current position of the mouse, we can simply ask the browser for it.

The way we are going to do this is by binding a "mousemove" event on our window object. As the name states, this will trigger whenever the mouse is moved and trigger a registered callback function. The callback will also be provided with an event object, which contains the mouse position information.


class EntropyCollector {
    constructor() {
        this.eventCallback = (event) => {
            this.collect(event);
        };
        window.addEventListener("mousemove", this.eventCallback);
    }

    collect(event) {
        // Get the current mouse position from the event object
        const {clientX: x, clientY: y} = event;
    }
}

var ec = new EntropyCollector();
    

What we got now is a class which on construct will register a "mousemove" event with a callback function "collect". This function gathers the current clientX and clientY from the provided event. This are our current x and y position of the mouse.

Since our plan is to collect entropy for password generation we need to store the positions we collected.

Also to make sure there is enough entropy collected, we need somehow to define how much positions must be collected in order to have a sufficient amount of "random" data.

To achieve both of these points we adjust the code like this


class EntropyCollector {
    constructor(intEntropyAmount) {
        this.intEntropyAmount = intEntropyAmount;
        this.pool = [];
        this.count = 0;
        this.eventCallback = (event) => {
            this.collect(event);
        };
        window.addEventListener("mousemove", this.eventCallback);
    }

    collect(event) {
        this.count++;

        // Get the current mouse position from the event object
        const { clientX: x, clientY: y } = event;
        
        this.pool.push(x * y);

        if (this.pool.length === this.intEntropyAmount) {
            window.removeEventListener('mousemove',this.eventCallback);
            console.log(this.pool);
        }
    }

}
var entropyAmount = 42;
var ec = new EntropyCollector(entropyAmount);
    

Output


Array(42) [ 108926, 103390, 92700, 84048, 76760, 69690, 64842, 61306, 60030, 59502, … ]
    

In this adjusted version the collect function will store the value of multiplied x and y positions as dataset in the classes pool array property.

Also the constructor will expect the target amount of datasets that should be collected as int. If the length of pool reaches the target amount, the "mousemove" event listener will be removed.

Since we now got data collected and we also have a point at which we can call a followup method on reaching the desired amount of data, we will add the possibility to provide a callback function which is called after unbinding the "mousemove" event. The function will be provided with the collected data as param. And since we're at it, we also add the possibility to add updateCallback which will be called whenever a datapoint has been collected. Using the updateCallback we can update the frontend so the user can see the actual progress.


class EntropyCollector {
    constructor(intEntropyAmount, updateCallback,finalCallback) {
        this.intEntropyAmount = intEntropyAmount;
        this.updateCallback = updateCallback;
        this.finalCallback = finalCallback;
        this.pool = [];
        this.count = 0;
        this.eventCallback = (event) => {
            this.collect(event);
        };
        window.addEventListener("mousemove", this.eventCallback);
    }

    collect(event) {
        this.count++;

        // Get the current mouse position from the event object
        const { clientX: x, clientY: y } = event;

        const lastPosition = this.pool[this.pool.length - 1];
        this.pool.push(x * y);

        this.updateCallback(this.pool.length, this.intEntropyAmount);

        if (this.pool.length === this.intEntropyAmount) {
            window.removeEventListener('mousemove',this.eventCallback);
            this.finalCallback(this.pool);
        }
    }
    
}

var entropyAmount = 42;
var updateCallback = function(poolLength, desiredAmount) {
    console.log("We are at " + poolLength + " of " + desiredAmount + " datasets");
};
var finalCallback = function(pool) {
    console.log("The reached the desired amount of datasets: ", pool);
};
var ec = new EntropyCollector(entropyAmount, updateCallback, finalCallback);
    

This actually provided us with some output - lets check it out!


We are at 1 of 42 datasets debugger eval code:35:13
We are at 2 of 42 datasets debugger eval code:35:13
We are at 3 of 42 datasets debugger eval code:35:13
We are at 4 of 42 datasets debugger eval code:35:13
We are at 5 of 42 datasets debugger eval code:35:13
We are at 6 of 42 datasets debugger eval code:35:13
We are at 7 of 42 datasets debugger eval code:35:13
We are at 8 of 42 datasets debugger eval code:35:13
We are at 9 of 42 datasets debugger eval code:35:13
We are at 10 of 42 datasets debugger eval code:35:13
We are at 11 of 42 datasets debugger eval code:35:13
We are at 12 of 42 datasets debugger eval code:35:13
We are at 13 of 42 datasets debugger eval code:35:13
We are at 14 of 42 datasets debugger eval code:35:13
We are at 15 of 42 datasets debugger eval code:35:13
We are at 16 of 42 datasets debugger eval code:35:13
We are at 17 of 42 datasets debugger eval code:35:13
We are at 18 of 42 datasets debugger eval code:35:13
We are at 19 of 42 datasets debugger eval code:35:13
We are at 20 of 42 datasets debugger eval code:35:13
We are at 21 of 42 datasets debugger eval code:35:13
We are at 22 of 42 datasets debugger eval code:35:13
We are at 23 of 42 datasets debugger eval code:35:13
We are at 24 of 42 datasets debugger eval code:35:13
We are at 25 of 42 datasets debugger eval code:35:13
We are at 26 of 42 datasets debugger eval code:35:13
We are at 27 of 42 datasets debugger eval code:35:13
We are at 28 of 42 datasets debugger eval code:35:13
We are at 29 of 42 datasets debugger eval code:35:13
We are at 30 of 42 datasets debugger eval code:35:13
We are at 31 of 42 datasets debugger eval code:35:13
We are at 32 of 42 datasets debugger eval code:35:13
We are at 33 of 42 datasets debugger eval code:35:13
We are at 34 of 42 datasets debugger eval code:35:13
We are at 35 of 42 datasets debugger eval code:35:13
We are at 36 of 42 datasets debugger eval code:35:13
We are at 37 of 42 datasets debugger eval code:35:13
We are at 38 of 42 datasets debugger eval code:35:13
We are at 39 of 42 datasets debugger eval code:35:13
We are at 40 of 42 datasets debugger eval code:35:13
We are at 41 of 42 datasets debugger eval code:35:13
We are at 42 of 42 datasets debugger eval code:35:13
The reached the desired amount of datasets:  
Array(42) [ 312912, 313760, 312650, 313224, 312290, 310952, 308448, 305900, 302133, 298724, … ]
    

We made it - we created a javascript code which will collect a defined amount of datasets from users mouse movement information that could be used as entropy to generate random strings such as passwords.

Looking at the data i spotted there are two things that work against us.
1. The mousemove event is fired at a very high rate. This means that the difference between two datasets can be very minimal. It also makes steps more "predictable" since its following the movements path.
2. Right now we also would collect the same position twice in a row. While one might argue that this also is "random" we are going to prevent this.

So to solve both of the just listed issues, and add some quality of life, we adjust the code one last time. The following code shows our final

entropyCollector github


class EntropyCollector {
    constructor(intEntropyAmount, updateCallback,finalCallback) {
        this.intEntropyAmount = intEntropyAmount;
        this.updateCallback = updateCallback;
        this.finalCallback = finalCallback;
        this.pool = [];
        this.interval = 20;
        this.count = 0;
    }
    
    run() {
        this.interval = 20;
        this.count = 0;
        this.eventCallback = (event) => {
            this.collect(event);
        };
        window.addEventListener("mousemove", this.eventCallback);
    }

    collect(event) {
        this.count++;
        if (this.count % this.interval != 0) {
            return;
        }
        // Get the current mouse position from the event object
        const { clientX: x, clientY: y } = event;

        const lastPosition = this.pool[this.pool.length - 1];
        if (!lastPosition || lastPosition[0] !== x && lastPosition[1] !== y) {
            this.pool.push(x * y);

            this.updateCallback(this.pool.length, this.intEntropyAmount);

            if (this.pool.length === this.intEntropyAmount) {
                window.removeEventListener('mousemove',this.eventCallback);
                this.finalCallback(this.pool);
                this.reset();
            }
        }
    }

    reset() {
        this.pool = [];
        this.count = 0;
    }

}

var entropyAmount = 42;
var updateCallback = function(poolLength, desiredAmount) {
    console.log("We are at " + poolLength + " of " + desiredAmount + " datasets");
};
var finalCallback = function(pool) {
    console.log("The reached the desired amount of datasets: ", pool);
    console.log(JSON.stringify(pool));
};
var ec = new EntropyCollector(entropyAmount, updateCallback, finalCallback);
ec.run();
    

By adding an interval and using modulo to only collect data if


if (this.count % this.interval != 0) {
    

which increases the likelihood that the ones collected will vary more.

And by comparing the last gathered position to our current gathered position


const lastPosition = this.pool[this.pool.length - 1];
if (!lastPosition || lastPosition[0] !== x && lastPosition[1] !== y) {
    

we can prevent the script from collecting the same values multiple times in a row.

For quality of life we move the actual starting of our collection run into a 'run' function while also adding a reset function. This way we can reuse the instance instead of creating a new instance for each run.

After running it another time the collected data points (as json) are:


[441207,482514,409472,322848,284711,249159,166000,84588,6390,83721,259000,534660,636918,690966,293184,88296,158446,227052,172095,106038,47268,19440,20907,9300,38080,280116,344286,337365,499464,380250,259233,106212,44268,113420,207394,299288,291600,119019,30450,75350,108350,128016]
    

Conclusion

So we achieved our goal. We created a vanilla javascript code which will collect user input, in this case mouse movement information, to be used as entropy source for randomness.

As mentioned earlier, this is just one of many sources that can be used to collect user behaviour based data. This is just one example to showcase the idea.

And it's very important to mention that even just in this example, there still is a lot of room for improvement and i would not suggest to use this plain in any production environment.

I hope this article will serve as an inspiration for some, and for others just be an enjoyable little dive into the world of user based entropy collection.

So long and thanks for all the fish.