Exploring Randomness in JavaScript

In my post about the creation Color Palette Utilities in Alpine.js randomness played a big role: each sample was generated as a composition of randomly selected Hue values (0..360)Saturation (0..100) and Lightness (0..100)When I was creating this demo, I came across Web Crypto API. I usually use the method when generating random values Math.random()but the MDN documentation mentions that Crypto.getRandomValues() safer. So I decided to give it a try. Crypto (with fallback to module Math (as needed). But it made me wonder if “more secure” really means “more random” for my use case.

See an example in my JavaScript Demos project on GitHub.

View the code in my JavaScript Demos project on GitHub.

Randomness, from a security perspective, matters. I'm not a security expert, but as far as I understand, a pseudo-random number generator (PRNG) is considered “secure” if the sequence of numbers it will produce, or has produced, cannot be calculated by an attacker.

When it comes to “random color generators” like my color palette utility, the concept of “random” is much more nebulous. In my case, the color generation is as random as it “feels” to the user. In other words, the effectiveness of randomness is part of the user experience (UX).

To this end, I want to try to generate some random visual elements using both Math.random()and crypto.getRandomValues()to see if one of the methods would be significantly different sensations. Each attempt will contain a randomly generated element. <canvas> and a randomly generated set of integers. Then I'll use my (deeply flawed) human intuition to figure out whether one method looks “better” than the other.

Method Math.random() works by returning a decimal value from 0 (inclusive) up to 1 (exclusively) This can be used to generate random integers by taking the result of randomness and multiplying it by the range of possible values.

In other words, if Math.random() will return 0.25you will choose the value that is closest to 25% in the given min-max range. And if Math.random() will return 0.97you will select the value that is closest to 97% in the given min-max range.

Method crypto.getRandomValues() works very differently. Instead of giving you a single value back, it expects to accept TypedArray with a pre-selected size (length). Then the method .getRandomValues() fills this array with random values, limited by the minimum/maximum that the given type can store.

To make this study easier, I want both approaches to work roughly the same. So instead of dealing with decimals in one algorithm and integers in the other, I'll convert the algorithms' results to decimals. This means I have to convert valuereturned .getRandomValues()into a decimal number (0..1):

value / ( maxValue + 1 )

I encapsulate this difference into two methods, randFloatWithMath() And randFloatWithCrypto():

/**
* С помощью модуля Math я возвращаю случайное число с плавающей запятой в диапазоне от 0 (включительно) до 1 (исключительно).
*/
function randFloatWithMath() {

	return Math.random();

}

/**
* С помощью модуля Crypto я возвращаю случайное число с плавающей запятой в диапазоне от 0 (включительно) до 1 (исключительно).
*/
function randFloatWithCrypto() {

	var [ randomInt ] = crypto.getRandomValues( new Uint32Array( 1 ) );
	var maxInt = 4294967295;

	return ( randomInt / ( maxInt + 1 ) );

}

Having these two methods, I can assign one of them to a variable randFloat()which can be used to generate random values ​​in a given range using any of the algorithms:

/**
* Я генерирую случайное целое число между заданными min и max, включительно.
*/
function randRange( min, max ) {

	return ( min + Math.floor( randFloat() * ( max - min + 1 ) ) );

}

Now let's move on to creating experiments. The user interface is small and works on Alpine.jsEach experiment uses the same Alpine.js component, but its constructor receives an argument that determines which implementation randFloat() will be used:

<!doctype html>
<html lang="en">
<head>
	<meta charset="utf-8" />
	<meta name="viewport" content="width=device-width, initial-scale=1" />
	<link rel="stylesheet" type="text/css" href="https://habr.com/ru/articles/825986/./main.css" />
</head>
<body>

	<h1>
		<!-- Изучение случайности в JavaScript --> Exploring Randomness In JavaScript
	</h1>

	<div class="side-by-side">
		<section x-data="Explore( 'math' )">
			<h2>
				<!-- Модуль Math --> Math Module
			</h2>

			<!-- Очень большое количество случайных координат {X,Y}. -->
			<canvas
				x-ref="canvas"
				width="320"
				height="320">
			</canvas>

			<!-- Небольшое количество случайных значений координат. -->
			<p x-ref="list"></p>

			<p>
				<!-- Длительность --> Duration: <span x-text="duration"></span>
			</p>
		</section>

		<section x-data="Explore( 'crypto' )">
			<h2>
				<!-- Модуль Crypto --> Crypto Module
			</h2>

			<!-- Очень большое количество случайных координат {X,Y}. -->
			<canvas
				x-ref="canvas"
				width="320"
				height="320">
			</canvas>

			<!-- Небольшое количество случайных значений координат. -->
			<p x-ref="list"></p>

			<p>
				<!-- Длительность --> Duration: <span x-text="duration"></span>ms
			</p>
		</section>
	</div>

	<script type="text/javascript" src="https://habr.com/ru/articles/825986/./main.js" defer></script>
	<script type="text/javascript" src="https://habr.com/ru/vendor/alpine/3.13.5/alpine.3.13.5.min.js" defer></script>

</body>
</html>

As you can see, each component x-data="Explore" contains two x-ref: canvas And list. When the component is initialized, it will fill these two x-ref random values ​​using methods fillCanvas() And fillList() respectively.

Here is my JavaScript / Alpine.js component:

/**
* С помощью модуля Math я возвращаю случайное число с плавающей запятой в диапазоне от 0 (включительно) до 1 (исключительно).
*/
function randFloatWithMath() {

	return Math.random();

}

/**
* С помощью модуля Crypto я возвращаю случайное число с плавающей запятой в диапазоне от 0 (включительно) до 1 (исключительно).
*/
function randFloatWithCrypto() {

	// Этот метод работает, заполняя массив случайными значениями заданного типа.
	// В нашем случае нам нужно только одно случайное значение, поэтому мы передадим массив
    // длиной 1.
	// --
	// Примечание: Для повышения производительности мы можем кэшировать типизированный массив и просто передавать
	// одну и ту же ссылку (это улучшает производительность вдвое). Но мы исследуем
	// случайность, а не производительность.
	var [ randomInt ] = crypto.getRandomValues( new Uint32Array( 1 ) );
	var maxInt = 4294967295;

	// В отличие от Math.random(), crypto генерирует нам целое число. Чтобы подставить его
	// в то же математическое уравнение, мы должны преобразовать целое число в десятичное,
	// чтобы получить такое же случайное значение.
	return ( randomInt / ( maxInt + 1 ) );

}

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

function Explore( algorithm ) {

	// Каждому компоненту Alpine.js назначается своя стратегия генерации случайных
	// чисел с плавающей запятой (0..1). В остальном компоненты ведут себя
	// одинаково.
	var randFloat = ( algorithm === "math" )
		? randFloatWithMath
		: randFloatWithCrypto
	;

	return {
		duration: 0,
		// Публичные методы.
		init: init,
		// Приватные методы.
		fillCanvas: fillCanvas,
		fillList: fillList,
		randRange: randRange
	}

	// ---
	// ПУБЛИЧНЫЕ МЕТОДЫ.
	// ---

	/**
	* Я инициализирую компонент Alpine.js.
	*/
	function init() {

		var startedAt = Date.now();

		this.fillCanvas();
		this.fillList();

		this.duration = ( Date.now() - startedAt );

	}

	// ---
	// ПРИВАТНЫЕ МЕТОДЫ.
	// ---

	/**
	* Я заполняю canvas случайными пикселями {X,Y}.
	*/
	function fillCanvas() {

		var pixelCount = 200000;
		var canvas = this.$refs.canvas;
		var width = canvas.width;
		var height = canvas.height;

		var context = canvas.getContext( "2d" );
		context.fillStyle = "deeppink";

		for ( var i = 0 ; i < pixelCount ; i++ ) {

			var x = this.randRange( 0, width );
			var y = this.randRange( 0, height );

			// По мере добавления новых пикселей изменяем их непрозрачность.
			// Я надеялся, что это поможет показать потенциальную кластеризацию значений.
            context.globalAlpha = ( i / pixelCount );
			context.fillRect( x, y, 1, 1 );

		}

	}

	/**
	* Я заполняю список случайными значениями от 0 до 9.
	*/
	function fillList() {

		var list = this.$refs.list;
		var valueCount = 105;
		var values = [];

		for ( var i = 0 ; i < valueCount ; i++ ) {

			values.push( this.randRange( 0, 9 ) );

		}

		list.textContent = values.join( " " );

	}

	/**
	* Я генерирую случайное целое число между заданными min и max, включительно.
	*/
	function randRange( min, max ) {

		return ( min + Math.floor( randFloat() * ( max - min + 1 ) ) );

	}

}

When we run this example, we get the following result:

As I said above, randomness from a human point of view is very vague. It is more related to sensationsthan with mathematical probabilities. For example, the probability that a row will appear two identical values, equal to the probability that there will be a row two different meanings. But for a person it is it is felt differently.

However, if you compare these random generation visualizations, none of them seem significantly different in terms of distribution. Of course, the module Crypto significantly slower (half of that is the cost of allocating resources for the TypedArray). But in terms of “feeling”, neither is better than the other.

I'll just say that when using generation in the color palette utility I probably didn't need to use the module Crypto – perhaps it was worth stopping at Math. It's much faster and it is felt just as random. I'll use the module Crypto to work with client-side cryptography (which I haven't had to do yet).

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *